Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { NextResponse, NextRequest } from "next/server";
import { PostgrestSingleResponse } from "@supabase/supabase-js";
import { Database, Constants } from "@repo/database/types.gen.ts";
import { createClient } from "~/utils/supabase/server";
import {
createApiResponse,
asPostgrestFailure,
handleRouteError,
} from "~/utils/supabase/apiUtils";

type ApiParams = Promise<{ target: string; fn: string; worker: string }>;
export type SegmentDataType = { params: ApiParams };

// POST the task status to the /supabase/sync-task/{function_name}/{target}/{worker} endpoint
export const POST = async (
request: NextRequest,
segmentData: SegmentDataType,
): Promise<NextResponse> => {
try {
const { target, fn, worker } = await segmentData.params;
const targetN = Number.parseInt(target);
if (isNaN(targetN)) {
return createApiResponse(
request,
asPostgrestFailure(`${target} is not a number`, "type"),
);
}
const infoS: string = await request.json();
if (
!(Constants.public.Enums.task_status as readonly string[]).includes(infoS)
) {
return createApiResponse(
request,
asPostgrestFailure(`${infoS} is not a task status`, "type"),
);
}
const info = infoS as Database["public"]["Enums"]["task_status"];
const supabase = await createClient();
const response = (await supabase.rpc("end_sync_task", {
s_target: targetN,
s_function: fn,
s_worker: worker,
s_status: info,
})) as PostgrestSingleResponse<boolean>;
// Transform 204 No Content to 200 OK with success indicator for API consistency
if (response.status === 204) {
response.data = true;
response.status = 200;
}

return createApiResponse(request, response);
} catch (e: unknown) {
return handleRouteError(request, e, "/api/supabase/sync-task");
}
};
88 changes: 88 additions & 0 deletions apps/website/app/api/supabase/sync-task/[fn]/[target]/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { NextResponse, NextRequest } from "next/server";
import type { PostgrestSingleResponse } from "@supabase/supabase-js";
import { createClient } from "~/utils/supabase/server";
import {
createApiResponse,
handleRouteError,
defaultOptionsHandler,
asPostgrestFailure,
} from "~/utils/supabase/apiUtils";

type SyncTaskInfo = {
worker: string;
timeout?: string;
task_interval?: string;
};

const SYNC_DEFAULTS: Partial<SyncTaskInfo> = {
timeout: "20s",
task_interval: "45s",
};

type ApiParams = Promise<{ target: string; fn: string }>;
export type SegmentDataType = { params: ApiParams };

// POST with the SyncTaskInfo to the /supabase/sync-task/{function_name}/{target} endpoint
export const POST = async (
request: NextRequest,
segmentData: SegmentDataType,
): Promise<NextResponse> => {
try {
const { target, fn } = await segmentData.params;
const targetN = Number.parseInt(target);
if (isNaN(targetN)) {
return createApiResponse(
request,
asPostgrestFailure(`${target} is not a number`, "type"),
);
}
const info: SyncTaskInfo = { ...SYNC_DEFAULTS, ...(await request.json()) };
if (!info.worker) {
return createApiResponse(
request,
asPostgrestFailure("Worker field is required", "invalid"),
);
}
const supabase = await createClient();
const response = (await supabase.rpc("propose_sync_task", {
s_target: targetN,
s_function: fn,
s_worker: info.worker,
timeout: info.timeout,
task_interval: info.task_interval,
})) as PostgrestSingleResponse<Date | null | boolean>;
if (response.data === null) {
// NextJS responses cannot handle null values, convert to boolean success indicator
response.data = true;
}

return createApiResponse(request, response);
} catch (e: unknown) {
return handleRouteError(request, e, "/api/supabase/route");
}
};

// GET the sync_info table from /supabase/sync-task/{function_name}/{target} (should not be necessary)
export const GET = async (
request: NextRequest,
segmentData: SegmentDataType,
): Promise<NextResponse> => {
const { target, fn } = await segmentData.params;
const targetN = Number.parseInt(target);
if (isNaN(targetN)) {
return createApiResponse(
request,
asPostgrestFailure(`${targetN} is not a number`, "type"),
);
}
const supabase = await createClient();
const response = await supabase
.from("sync_info")
.select()
.eq("sync_target", targetN)
.eq("sync_function", fn)
.maybeSingle();
return createApiResponse(request, response);
};

export const OPTIONS = defaultOptionsHandler;
6 changes: 3 additions & 3 deletions packages/database/doc/sync_functions.md
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
# Sync information

The `sync_info` table is meant to always be accessed through one of these two functions: `propose_sync_task` and `end_sync_task`.
The `sync_info` table is meant to always be accessed through one of these two functions: `propose_sync_task` and `end_sync_task`, used through POSTS to the web api endpoints `api/supabase/sync-task/[fn]/[target]` and `api/supabase/sync-task/[fn]/[target]/[worker]` respectively.
This acts as a semaphore, so that two workers (e.g. the roam plugin on two different browsers) do not try to run the same sync task at the same time. So you need to give the function `propose_sync_task` enough information to distinguish what you mean to do:

1. The `target`, e.g. the database Id of the scope of the task, usually a space, but it could be a single content or concept (for reactive updates)
2. a `function` name, to distinguish different tasks on the same target; e.g. adding vs deleting content. (arbitrary short string)
3. the `worker` name: random string, should be the same between calls.
2. a `function` name, to distinguish different tasks on the same target; e.g. adding vs deleting content. (arbitrary short string)
3. the `worker` name: random string, should be the same between calls.

Further, you may specify the `timeout` (>= 1s) after which the task should be deemed to have failed. The `task_interval` (>=5s) which is how often to do the task. (This must be longer than the `timeout`.)

Expand Down