syncsnap 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,7 @@
1
+ # syncsnap
2
+
3
+ Syncsnap server SDK for Node and Next.js — API client and route handlers for file sync.
4
+
5
+ ## Docs
6
+
7
+ **[Documentation →](https://docs.syncsnap.com)**
@@ -0,0 +1,60 @@
1
+ type JobStatus = 'pending' | 'in_progress' | 'completed' | 'failed';
2
+ interface Job {
3
+ id: string;
4
+ projectId: string;
5
+ status: JobStatus;
6
+ fileName?: string | null;
7
+ createdAt: string;
8
+ updatedAt: string;
9
+ }
10
+ interface CreateJobResponse {
11
+ id: string;
12
+ projectId: string;
13
+ status: JobStatus;
14
+ createdAt: string;
15
+ updatedAt: string;
16
+ }
17
+ interface PresignedUrlResponse {
18
+ url: string;
19
+ fileName: string;
20
+ expiration: string;
21
+ }
22
+ /**
23
+ * Response for the download URL endpoint. When the server is configured with
24
+ * `onCompleted`, its return value is included as `completedPayload`.
25
+ */
26
+ interface DownloadUrlResponse<T = unknown> extends PresignedUrlResponse {
27
+ completedPayload?: T;
28
+ }
29
+ interface WaitForJobOptions {
30
+ intervalMs?: number;
31
+ timeoutMs?: number;
32
+ onPoll?: (job: Job) => void;
33
+ signal?: AbortSignal;
34
+ }
35
+ /** Response from the wait-for-completion endpoint. `result` is whatever the server's onCompleted callback returned. */
36
+ interface WaitCompletionResponse<T = unknown> {
37
+ job: Job;
38
+ result?: T;
39
+ }
40
+
41
+ declare class SyncsnapServer {
42
+ private readonly baseUrl;
43
+ private readonly token;
44
+ private readonly fetcher;
45
+ constructor(apiVersion?: string);
46
+ private authHeaders;
47
+ private requestJson;
48
+ createJob(): Promise<CreateJobResponse>;
49
+ getJob(jobId: string): Promise<Job>;
50
+ getUploadUrl(jobId: string, options: {
51
+ fileName: string;
52
+ expirationMinutes?: number;
53
+ }): Promise<PresignedUrlResponse>;
54
+ getDownloadUrl(jobId: string, options?: {
55
+ expirationMinutes?: number;
56
+ }): Promise<PresignedUrlResponse>;
57
+ waitForJobCompletion(jobId: string, options?: WaitForJobOptions): Promise<Job>;
58
+ }
59
+
60
+ export { type CreateJobResponse, type DownloadUrlResponse, type Job, type JobStatus, type PresignedUrlResponse, SyncsnapServer, type WaitCompletionResponse, type WaitForJobOptions };
package/dist/index.js ADDED
@@ -0,0 +1,86 @@
1
+ // src/client.ts
2
+ var SYNCSNAP_API_BASE_URL = "https://api.syncsnap.xyz/api";
3
+ var SyncsnapServer = class {
4
+ constructor(apiVersion = "v1") {
5
+ this.baseUrl = `${SYNCSNAP_API_BASE_URL}/${apiVersion}`;
6
+ this.token = process.env.SYNCSNAP_TOKEN;
7
+ this.fetcher = fetch;
8
+ }
9
+ authHeaders() {
10
+ return {
11
+ "Content-Type": "application/json",
12
+ "X-API-KEY": this.token
13
+ };
14
+ }
15
+ async requestJson(url, init) {
16
+ const res = await this.fetcher(url, init);
17
+ const data = await res.json().catch(() => ({}));
18
+ if (!res.ok) {
19
+ const message = typeof data === "object" && data && "error" in data && data.error ? String(data.error) : `Syncsnap request failed (${res.status})`;
20
+ throw new Error(message);
21
+ }
22
+ return data;
23
+ }
24
+ async createJob() {
25
+ const url = `${this.baseUrl}/jobs`;
26
+ return this.requestJson(url, {
27
+ method: "POST",
28
+ headers: this.authHeaders()
29
+ });
30
+ }
31
+ async getJob(jobId) {
32
+ const url = `${this.baseUrl}/jobs/${encodeURIComponent(jobId)}`;
33
+ return this.requestJson(url, {
34
+ method: "GET",
35
+ headers: this.authHeaders()
36
+ });
37
+ }
38
+ async getUploadUrl(jobId, options) {
39
+ const url = `${this.baseUrl}/jobs/${encodeURIComponent(
40
+ jobId
41
+ )}/presigned-upload-url`;
42
+ return this.requestJson(url, {
43
+ method: "POST",
44
+ headers: this.authHeaders(),
45
+ body: JSON.stringify({
46
+ file_name: options.fileName,
47
+ expiration: options.expirationMinutes
48
+ })
49
+ });
50
+ }
51
+ async getDownloadUrl(jobId, options) {
52
+ const url = `${this.baseUrl}/jobs/${encodeURIComponent(
53
+ jobId
54
+ )}/presigned-download-url`;
55
+ return this.requestJson(url, {
56
+ method: "POST",
57
+ headers: this.authHeaders(),
58
+ body: JSON.stringify({
59
+ expiration: options?.expirationMinutes
60
+ })
61
+ });
62
+ }
63
+ async waitForJobCompletion(jobId, options = {}) {
64
+ const intervalMs = options.intervalMs ?? 2e3;
65
+ const timeoutMs = options.timeoutMs ?? 12e4;
66
+ const startedAt = Date.now();
67
+ while (true) {
68
+ if (options.signal?.aborted) {
69
+ throw new Error("Polling aborted");
70
+ }
71
+ if (Date.now() - startedAt > timeoutMs) {
72
+ throw new Error("Polling timed out");
73
+ }
74
+ const job = await this.getJob(jobId);
75
+ options.onPoll?.(job);
76
+ if (job.status === "completed" || job.status === "failed") {
77
+ return job;
78
+ }
79
+ await new Promise((resolve) => setTimeout(resolve, intervalMs));
80
+ }
81
+ }
82
+ };
83
+ export {
84
+ SyncsnapServer
85
+ };
86
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/client.ts"],"sourcesContent":["import type {\n CreateJobResponse,\n Job,\n PresignedUrlResponse,\n WaitForJobOptions,\n} from './types';\n\nconst SYNCSNAP_API_BASE_URL = 'https://api.syncsnap.xyz/api';\n\nexport class SyncsnapServer {\n private readonly baseUrl: string;\n private readonly token: string;\n private readonly fetcher: typeof fetch;\n\n constructor(apiVersion: string = 'v1') {\n this.baseUrl = `${SYNCSNAP_API_BASE_URL}/${apiVersion}`;\n this.token = process.env.SYNCSNAP_TOKEN!;\n this.fetcher = fetch;\n }\n\n private authHeaders(): HeadersInit {\n return {\n 'Content-Type': 'application/json',\n 'X-API-KEY': this.token,\n };\n }\n\n private async requestJson<T>(url: string, init?: RequestInit): Promise<T> {\n const res = await this.fetcher(url, init);\n const data = (await res.json().catch(() => ({}))) as T & {\n error?: string;\n };\n\n if (!res.ok) {\n const message =\n typeof data === 'object' && data && 'error' in data && data.error\n ? String(data.error)\n : `Syncsnap request failed (${res.status})`;\n throw new Error(message);\n }\n\n return data as T;\n }\n\n async createJob(): Promise<CreateJobResponse> {\n const url = `${this.baseUrl}/jobs`;\n return this.requestJson<CreateJobResponse>(url, {\n method: 'POST',\n headers: this.authHeaders(),\n });\n }\n\n async getJob(jobId: string): Promise<Job> {\n const url = `${this.baseUrl}/jobs/${encodeURIComponent(jobId)}`;\n return this.requestJson<Job>(url, {\n method: 'GET',\n headers: this.authHeaders(),\n });\n }\n\n async getUploadUrl(\n jobId: string,\n options: { fileName: string; expirationMinutes?: number }\n ): Promise<PresignedUrlResponse> {\n const url = `${this.baseUrl}/jobs/${encodeURIComponent(\n jobId\n )}/presigned-upload-url`;\n return this.requestJson<PresignedUrlResponse>(url, {\n method: 'POST',\n headers: this.authHeaders(),\n body: JSON.stringify({\n file_name: options.fileName,\n expiration: options.expirationMinutes,\n }),\n });\n }\n\n async getDownloadUrl(\n jobId: string,\n options?: { expirationMinutes?: number }\n ): Promise<PresignedUrlResponse> {\n const url = `${this.baseUrl}/jobs/${encodeURIComponent(\n jobId\n )}/presigned-download-url`;\n return this.requestJson<PresignedUrlResponse>(url, {\n method: 'POST',\n headers: this.authHeaders(),\n body: JSON.stringify({\n expiration: options?.expirationMinutes,\n }),\n });\n }\n\n async waitForJobCompletion(\n jobId: string,\n options: WaitForJobOptions = {}\n ): Promise<Job> {\n const intervalMs = options.intervalMs ?? 2000;\n const timeoutMs = options.timeoutMs ?? 120000;\n const startedAt = Date.now();\n\n while (true) {\n if (options.signal?.aborted) {\n throw new Error('Polling aborted');\n }\n if (Date.now() - startedAt > timeoutMs) {\n throw new Error('Polling timed out');\n }\n\n const job = await this.getJob(jobId);\n options.onPoll?.(job);\n\n if (job.status === 'completed' || job.status === 'failed') {\n return job;\n }\n\n await new Promise((resolve) => setTimeout(resolve, intervalMs));\n }\n }\n}\n"],"mappings":";AAOA,IAAM,wBAAwB;AAEvB,IAAM,iBAAN,MAAqB;AAAA,EAK1B,YAAY,aAAqB,MAAM;AACrC,SAAK,UAAU,GAAG,qBAAqB,IAAI,UAAU;AACrD,SAAK,QAAQ,QAAQ,IAAI;AACzB,SAAK,UAAU;AAAA,EACjB;AAAA,EAEQ,cAA2B;AACjC,WAAO;AAAA,MACL,gBAAgB;AAAA,MAChB,aAAa,KAAK;AAAA,IACpB;AAAA,EACF;AAAA,EAEA,MAAc,YAAe,KAAa,MAAgC;AACxE,UAAM,MAAM,MAAM,KAAK,QAAQ,KAAK,IAAI;AACxC,UAAM,OAAQ,MAAM,IAAI,KAAK,EAAE,MAAM,OAAO,CAAC,EAAE;AAI/C,QAAI,CAAC,IAAI,IAAI;AACX,YAAM,UACJ,OAAO,SAAS,YAAY,QAAQ,WAAW,QAAQ,KAAK,QACxD,OAAO,KAAK,KAAK,IACjB,4BAA4B,IAAI,MAAM;AAC5C,YAAM,IAAI,MAAM,OAAO;AAAA,IACzB;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,YAAwC;AAC5C,UAAM,MAAM,GAAG,KAAK,OAAO;AAC3B,WAAO,KAAK,YAA+B,KAAK;AAAA,MAC9C,QAAQ;AAAA,MACR,SAAS,KAAK,YAAY;AAAA,IAC5B,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,OAAO,OAA6B;AACxC,UAAM,MAAM,GAAG,KAAK,OAAO,SAAS,mBAAmB,KAAK,CAAC;AAC7D,WAAO,KAAK,YAAiB,KAAK;AAAA,MAChC,QAAQ;AAAA,MACR,SAAS,KAAK,YAAY;AAAA,IAC5B,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,aACJ,OACA,SAC+B;AAC/B,UAAM,MAAM,GAAG,KAAK,OAAO,SAAS;AAAA,MAClC;AAAA,IACF,CAAC;AACD,WAAO,KAAK,YAAkC,KAAK;AAAA,MACjD,QAAQ;AAAA,MACR,SAAS,KAAK,YAAY;AAAA,MAC1B,MAAM,KAAK,UAAU;AAAA,QACnB,WAAW,QAAQ;AAAA,QACnB,YAAY,QAAQ;AAAA,MACtB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,eACJ,OACA,SAC+B;AAC/B,UAAM,MAAM,GAAG,KAAK,OAAO,SAAS;AAAA,MAClC;AAAA,IACF,CAAC;AACD,WAAO,KAAK,YAAkC,KAAK;AAAA,MACjD,QAAQ;AAAA,MACR,SAAS,KAAK,YAAY;AAAA,MAC1B,MAAM,KAAK,UAAU;AAAA,QACnB,YAAY,SAAS;AAAA,MACvB,CAAC;AAAA,IACH,CAAC;AAAA,EACH;AAAA,EAEA,MAAM,qBACJ,OACA,UAA6B,CAAC,GAChB;AACd,UAAM,aAAa,QAAQ,cAAc;AACzC,UAAM,YAAY,QAAQ,aAAa;AACvC,UAAM,YAAY,KAAK,IAAI;AAE3B,WAAO,MAAM;AACX,UAAI,QAAQ,QAAQ,SAAS;AAC3B,cAAM,IAAI,MAAM,iBAAiB;AAAA,MACnC;AACA,UAAI,KAAK,IAAI,IAAI,YAAY,WAAW;AACtC,cAAM,IAAI,MAAM,mBAAmB;AAAA,MACrC;AAEA,YAAM,MAAM,MAAM,KAAK,OAAO,KAAK;AACnC,cAAQ,SAAS,GAAG;AAEpB,UAAI,IAAI,WAAW,eAAe,IAAI,WAAW,UAAU;AACzD,eAAO;AAAA,MACT;AAEA,YAAM,IAAI,QAAQ,CAAC,YAAY,WAAW,SAAS,UAAU,CAAC;AAAA,IAChE;AAAA,EACF;AACF;","names":[]}
package/dist/next.d.ts ADDED
@@ -0,0 +1,30 @@
1
+ import { SyncsnapServer, Job, PresignedUrlResponse } from './index.js';
2
+
3
+ type NextParams = {
4
+ params: {
5
+ id: string;
6
+ } | Promise<{
7
+ id: string;
8
+ }>;
9
+ };
10
+ type CatchAllParams = {
11
+ params?: Record<string, string | string[] | undefined> | Promise<Record<string, string | string[] | undefined>>;
12
+ };
13
+ declare function createJobHandler(client: SyncsnapServer): () => Promise<Response>;
14
+ declare function getJobHandler(client: SyncsnapServer): (_request: Request, context: NextParams) => Promise<Response>;
15
+ declare function getDownloadUrlHandler(client: SyncsnapServer): (request: Request, context: NextParams) => Promise<Response>;
16
+ interface CreateRouteHandlerOptions {
17
+ client: SyncsnapServer;
18
+ /**
19
+ * Called when a job completes (after server-side polling). Receives the job and, when
20
+ * status is "completed", the presigned download URL. Whatever this returns is sent to
21
+ * the client and passed to `useSyncsnapJob`'s `onCompleted(job, result)`.
22
+ */
23
+ onCompleted?: (job: Job, presigned?: PresignedUrlResponse) => unknown | Promise<unknown>;
24
+ }
25
+ declare function createRouteHandler(options: CreateRouteHandlerOptions): {
26
+ GET: (request: Request, context: CatchAllParams) => Promise<Response>;
27
+ POST: (_request: Request, context: CatchAllParams) => Promise<Response>;
28
+ };
29
+
30
+ export { type CreateRouteHandlerOptions, Job, createJobHandler, createRouteHandler, getDownloadUrlHandler, getJobHandler };
package/dist/next.js ADDED
@@ -0,0 +1,155 @@
1
+ // src/next/route-handlers.ts
2
+ function json(data, init) {
3
+ return new Response(JSON.stringify(data), {
4
+ ...init,
5
+ headers: {
6
+ "Content-Type": "application/json",
7
+ ...init?.headers ?? {}
8
+ }
9
+ });
10
+ }
11
+ function parseExpiration(url) {
12
+ const { searchParams } = new URL(url);
13
+ const raw = searchParams.get("expiration");
14
+ if (!raw) return void 0;
15
+ const value = Number(raw);
16
+ return Number.isFinite(value) && value > 0 ? value : void 0;
17
+ }
18
+ function parseWaitOptions(url) {
19
+ const { searchParams } = new URL(url);
20
+ const timeoutMs = parseOptionalPositiveNumber(searchParams.get("timeoutMs"));
21
+ const intervalMs = parseOptionalPositiveNumber(
22
+ searchParams.get("intervalMs")
23
+ );
24
+ return { timeoutMs, intervalMs };
25
+ }
26
+ function parseOptionalPositiveNumber(value) {
27
+ if (value == null) return void 0;
28
+ const n = Number(value);
29
+ return Number.isFinite(n) && n > 0 ? n : void 0;
30
+ }
31
+ async function resolveParams(params) {
32
+ if (!params) return void 0;
33
+ if (typeof params.then === "function") {
34
+ return await params;
35
+ }
36
+ return params;
37
+ }
38
+ function getCatchAllPath(params) {
39
+ if (!params) return [];
40
+ for (const value of Object.values(params)) {
41
+ if (Array.isArray(value)) return value;
42
+ if (typeof value === "string" && value.length > 0) return [value];
43
+ }
44
+ return [];
45
+ }
46
+ function createJobHandler(client) {
47
+ return async function POST() {
48
+ const job = await client.createJob();
49
+ return json(job);
50
+ };
51
+ }
52
+ function getJobHandler(client) {
53
+ return async function GET(_request, context) {
54
+ const resolved = await resolveParams(context.params);
55
+ if (!resolved?.id) {
56
+ return json({ error: "Job id is required" }, { status: 400 });
57
+ }
58
+ const job = await client.getJob(resolved.id);
59
+ return json(job);
60
+ };
61
+ }
62
+ function getDownloadUrlHandler(client) {
63
+ return async function GET(request, context) {
64
+ const resolved = await resolveParams(context.params);
65
+ if (!resolved?.id) {
66
+ return json({ error: "Job id is required" }, { status: 400 });
67
+ }
68
+ const expirationMinutes = parseExpiration(request.url);
69
+ const response = await client.getDownloadUrl(
70
+ resolved.id,
71
+ { expirationMinutes }
72
+ );
73
+ return json(response);
74
+ };
75
+ }
76
+ function createRouteHandler(options) {
77
+ const { client, onCompleted: onCompletedCallback } = options;
78
+ return {
79
+ GET: async (request, context) => {
80
+ const resolved = await resolveParams(context.params);
81
+ const segments = getCatchAllPath(resolved);
82
+ if (segments.length === 2 && segments[0] === "job") {
83
+ const job = await client.getJob(segments[1]);
84
+ return json(job);
85
+ }
86
+ if (segments.length === 3 && segments[0] === "job" && segments[2] === "download") {
87
+ const jobId = segments[1];
88
+ const job = await client.getJob(jobId);
89
+ if (job.status !== "completed") {
90
+ return json({ error: "Job is not completed" }, { status: 400 });
91
+ }
92
+ const expirationMinutes = parseExpiration(request.url);
93
+ const presigned = await client.getDownloadUrl(
94
+ jobId,
95
+ {
96
+ expirationMinutes
97
+ }
98
+ );
99
+ const response = { ...presigned };
100
+ if (onCompletedCallback) {
101
+ const payload = await onCompletedCallback(job, presigned);
102
+ if (payload !== void 0) {
103
+ response.completedPayload = payload;
104
+ }
105
+ }
106
+ return json(response);
107
+ }
108
+ if (segments.length === 3 && segments[0] === "job" && segments[2] === "wait") {
109
+ const jobId = segments[1];
110
+ const { timeoutMs, intervalMs } = parseWaitOptions(request.url);
111
+ try {
112
+ const job = await client.waitForJobCompletion(jobId, {
113
+ timeoutMs,
114
+ intervalMs
115
+ });
116
+ const body = { job };
117
+ if (job.status === "completed") {
118
+ const presigned = await client.getDownloadUrl(jobId);
119
+ const result = onCompletedCallback ? await onCompletedCallback(job, presigned) : presigned;
120
+ if (result !== void 0) {
121
+ body.result = result;
122
+ }
123
+ } else if (onCompletedCallback) {
124
+ const result = await onCompletedCallback(job);
125
+ if (result !== void 0) {
126
+ body.result = result;
127
+ }
128
+ }
129
+ return json(body);
130
+ } catch (err) {
131
+ const message = err instanceof Error ? err.message : "Wait failed";
132
+ const isTimeout = message.includes("timed out") || message.includes("Polling timed out");
133
+ return json({ error: message }, { status: isTimeout ? 408 : 500 });
134
+ }
135
+ }
136
+ return json({ error: "Not found" }, { status: 404 });
137
+ },
138
+ POST: async (_request, context) => {
139
+ const resolved = await resolveParams(context.params);
140
+ const segments = getCatchAllPath(resolved);
141
+ if (segments.length === 1 && segments[0] === "job") {
142
+ const job = await client.createJob();
143
+ return json(job);
144
+ }
145
+ return json({ error: "Not found" }, { status: 404 });
146
+ }
147
+ };
148
+ }
149
+ export {
150
+ createJobHandler,
151
+ createRouteHandler,
152
+ getDownloadUrlHandler,
153
+ getJobHandler
154
+ };
155
+ //# sourceMappingURL=next.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/next/route-handlers.ts"],"sourcesContent":["import type { SyncsnapServer } from '../client';\nimport type {\n DownloadUrlResponse,\n Job,\n PresignedUrlResponse,\n WaitCompletionResponse,\n} from '../types';\n\ntype NextParams = { params: { id: string } | Promise<{ id: string }> };\ntype CatchAllParams = {\n params?:\n | Record<string, string | string[] | undefined>\n | Promise<Record<string, string | string[] | undefined>>;\n};\n\nfunction json(data: unknown, init?: ResponseInit): Response {\n return new Response(JSON.stringify(data), {\n ...init,\n headers: {\n 'Content-Type': 'application/json',\n ...(init?.headers ?? {}),\n },\n });\n}\n\nfunction parseExpiration(url: string): number | undefined {\n const { searchParams } = new URL(url);\n const raw = searchParams.get('expiration');\n if (!raw) return undefined;\n const value = Number(raw);\n return Number.isFinite(value) && value > 0 ? value : undefined;\n}\n\nfunction parseWaitOptions(url: string): {\n timeoutMs?: number;\n intervalMs?: number;\n} {\n const { searchParams } = new URL(url);\n const timeoutMs = parseOptionalPositiveNumber(searchParams.get('timeoutMs'));\n const intervalMs = parseOptionalPositiveNumber(\n searchParams.get('intervalMs')\n );\n return { timeoutMs, intervalMs };\n}\n\nfunction parseOptionalPositiveNumber(value: string | null): number | undefined {\n if (value == null) return undefined;\n const n = Number(value);\n return Number.isFinite(n) && n > 0 ? n : undefined;\n}\n\nasync function resolveParams<T>(\n params?: T | Promise<T>\n): Promise<T | undefined> {\n if (!params) return undefined;\n if (typeof (params as Promise<T>).then === 'function') {\n return await (params as Promise<T>);\n }\n return params as T;\n}\n\nfunction getCatchAllPath(\n params?: Record<string, string | string[] | undefined>\n): string[] {\n if (!params) return [];\n for (const value of Object.values(params)) {\n if (Array.isArray(value)) return value;\n if (typeof value === 'string' && value.length > 0) return [value];\n }\n return [];\n}\n\nexport function createJobHandler(client: SyncsnapServer) {\n return async function POST(): Promise<Response> {\n const job = await client.createJob();\n return json(job);\n };\n}\n\nexport function getJobHandler(client: SyncsnapServer) {\n return async function GET(\n _request: Request,\n context: NextParams\n ): Promise<Response> {\n const resolved = await resolveParams(context.params);\n if (!resolved?.id) {\n return json({ error: 'Job id is required' }, { status: 400 });\n }\n const job = await client.getJob(resolved.id);\n return json(job);\n };\n}\n\nexport function getDownloadUrlHandler(client: SyncsnapServer) {\n return async function GET(\n request: Request,\n context: NextParams\n ): Promise<Response> {\n const resolved = await resolveParams(context.params);\n if (!resolved?.id) {\n return json({ error: 'Job id is required' }, { status: 400 });\n }\n const expirationMinutes = parseExpiration(request.url);\n const response: PresignedUrlResponse = await client.getDownloadUrl(\n resolved.id,\n { expirationMinutes }\n );\n return json(response);\n };\n}\n\nexport interface CreateRouteHandlerOptions {\n client: SyncsnapServer;\n /**\n * Called when a job completes (after server-side polling). Receives the job and, when\n * status is \"completed\", the presigned download URL. Whatever this returns is sent to\n * the client and passed to `useSyncsnapJob`'s `onCompleted(job, result)`.\n */\n onCompleted?: (\n job: Job,\n presigned?: PresignedUrlResponse\n ) => unknown | Promise<unknown>;\n}\n\nexport function createRouteHandler(options: CreateRouteHandlerOptions) {\n const { client, onCompleted: onCompletedCallback } = options;\n\n return {\n GET: async (\n request: Request,\n context: CatchAllParams\n ): Promise<Response> => {\n const resolved = await resolveParams(context.params);\n const segments = getCatchAllPath(resolved);\n if (segments.length === 2 && segments[0] === 'job') {\n const job = await client.getJob(segments[1]);\n return json(job);\n }\n\n if (\n segments.length === 3 &&\n segments[0] === 'job' &&\n segments[2] === 'download'\n ) {\n const jobId = segments[1];\n const job = await client.getJob(jobId);\n if (job.status !== 'completed') {\n return json({ error: 'Job is not completed' }, { status: 400 });\n }\n const expirationMinutes = parseExpiration(request.url);\n const presigned: PresignedUrlResponse = await client.getDownloadUrl(\n jobId,\n {\n expirationMinutes,\n }\n );\n const response: DownloadUrlResponse = { ...presigned };\n if (onCompletedCallback) {\n const payload = await onCompletedCallback(job, presigned);\n if (payload !== undefined) {\n response.completedPayload = payload;\n }\n }\n return json(response);\n }\n\n if (\n segments.length === 3 &&\n segments[0] === 'job' &&\n segments[2] === 'wait'\n ) {\n const jobId = segments[1];\n const { timeoutMs, intervalMs } = parseWaitOptions(request.url);\n try {\n const job = await client.waitForJobCompletion(jobId, {\n timeoutMs,\n intervalMs,\n });\n const body: WaitCompletionResponse = { job };\n if (job.status === 'completed') {\n const presigned: PresignedUrlResponse =\n await client.getDownloadUrl(jobId);\n const result = onCompletedCallback\n ? await onCompletedCallback(job, presigned)\n : presigned;\n if (result !== undefined) {\n body.result = result;\n }\n } else if (onCompletedCallback) {\n const result = await onCompletedCallback(job);\n if (result !== undefined) {\n body.result = result;\n }\n }\n return json(body);\n } catch (err) {\n const message = err instanceof Error ? err.message : 'Wait failed';\n const isTimeout =\n message.includes('timed out') ||\n message.includes('Polling timed out');\n return json({ error: message }, { status: isTimeout ? 408 : 500 });\n }\n }\n\n return json({ error: 'Not found' }, { status: 404 });\n },\n POST: async (\n _request: Request,\n context: CatchAllParams\n ): Promise<Response> => {\n const resolved = await resolveParams(context.params);\n const segments = getCatchAllPath(resolved);\n if (segments.length === 1 && segments[0] === 'job') {\n const job = await client.createJob();\n return json(job);\n }\n\n return json({ error: 'Not found' }, { status: 404 });\n },\n };\n}\n\nexport type { Job };\n"],"mappings":"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","names":[]}
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "syncsnap",
3
+ "version": "1.0.0",
4
+ "description": "Syncsnap server SDK for Node/Next.js",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "module": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "default": "./dist/index.js"
13
+ },
14
+ "./next": {
15
+ "types": "./dist/next.d.ts",
16
+ "default": "./dist/next.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "repository": {
23
+ "type": "git",
24
+ "url": "https://github.com/syncsnap/packages.git",
25
+ "directory": "syncsnap"
26
+ },
27
+ "homepage": "https://github.com/syncsnap/packages/syncsnap#readme",
28
+ "bugs": {
29
+ "url": "https://github.com/syncsnap/packages/syncsnap/issues"
30
+ },
31
+ "keywords": [
32
+ "syncsnap",
33
+ "server",
34
+ "sdk",
35
+ "next.js",
36
+ "api",
37
+ "file-sync"
38
+ ],
39
+ "license": "MIT",
40
+ "devDependencies": {
41
+ "@types/node": "^25.2.2",
42
+ "tsup": "^8.0.1",
43
+ "typescript": "^5.4.0",
44
+ "vitest": "^2.1.0"
45
+ },
46
+ "scripts": {
47
+ "lint": "eslint . --max-warnings 0",
48
+ "lint:fix": "eslint . --fix",
49
+ "format": "prettier --write .",
50
+ "format:check": "prettier --check .",
51
+ "build": "tsup src/index.ts src/next.ts --dts --format esm --out-dir dist --clean --sourcemap",
52
+ "publish": "npm publish --provenance --access public",
53
+ "link": "npm run build && npm link",
54
+ "prepublishOnly": "npm run build",
55
+ "test": "vitest run",
56
+ "test:watch": "vitest"
57
+ }
58
+ }