@svadmin/supabase 0.13.8 → 0.15.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/src/supacloud.ts CHANGED
@@ -1,660 +1,286 @@
1
- import type {
2
- LiveEvent,
3
- LiveProvider,
4
- SubmitTaskOptions,
5
- TaskHandle,
6
- TaskListResult,
7
- TaskProvider,
8
- TaskRecord,
9
- TaskSubscription,
10
- } from '@svadmin/core';
11
-
12
- export interface SupaCloudTaskRecord extends TaskRecord {
13
- [key: string]: unknown;
14
- }
15
-
16
- export interface SupaCloudSdkTaskRecord {
17
- id: string;
18
- status: string;
19
- progress?: number | null;
20
- error?: string | null;
21
- error_message?: string | null;
22
- updatedAt?: string | null;
23
- updated_at?: string | null;
24
- [key: string]: unknown;
25
- }
26
-
27
- export interface SupaCloudTaskSnapshot {
28
- id: string;
29
- status: string;
30
- progress?: number | null;
31
- error?: string | null;
32
- updatedAt?: string | null;
33
- raw: unknown;
34
- }
35
-
36
- export type SupaCloudTaskSubscribeState = 'connecting' | 'realtime' | 'polling' | 'closed';
37
-
38
- export interface SupaCloudTaskSubscribeOptions<TTask = SupaCloudTaskSnapshot> {
39
- pollingIntervalMs?: number;
40
- realtimeTimeoutMs?: number;
41
- reconcileIntervalMs?: number;
42
- onUpdate: (task: TTask) => void;
43
- onStateChange?: (state: SupaCloudTaskSubscribeState, details?: { error?: unknown }) => void;
44
- onError?: (error: unknown) => void;
45
- stopOnTerminal?: boolean;
46
- }
47
-
48
- export interface SupaCloudSdkTaskWaitOptions {
49
- intervalMs?: number;
50
- signal?: AbortSignal;
51
- }
52
-
53
- export interface SupaCloudSdkTaskSubmitOptions {
54
- body?: string
55
- | Blob
56
- | ArrayBuffer
57
- | FormData
58
- | File
59
- | ReadableStream<Uint8Array>
60
- | Record<string, unknown>;
1
+ import { Type, type Static, type TSchema } from '@sinclair/typebox';
2
+ import type { LiveEvent, LiveProvider, TaskHandle, TaskProvider, TaskRecord, SubmitTaskOptions } from '@svadmin/core';
3
+ import { definedOptions } from '@svadmin/core/options';
4
+ import { checkExact, decodeTaskRecord, decodeTaskSubmitOptions, snapshotPlainData, TaskError, validatedTaskSubscription, taskClientField, requiredTaskClientMethod } from '@svadmin/core/schema';
5
+
6
+ const text = Type.String({ minLength: 1 });
7
+ const idSchema = Type.String({ pattern: '^[A-Za-z0-9][A-Za-z0-9_.:-]*$' });
8
+ const nameSchema = Type.String({ pattern: '^[A-Za-z0-9][A-Za-z0-9_.-]*(/[A-Za-z0-9][A-Za-z0-9_.-]*)*$' });
9
+ const statusSchema = Type.Union([
10
+ Type.Literal('pending'), Type.Literal('leased'), Type.Literal('running'),
11
+ Type.Literal('retry_scheduled'), Type.Literal('succeeded'), Type.Literal('failed'),
12
+ Type.Literal('dead_lettered'), Type.Literal('cancelled'), Type.Literal('queued'),
13
+ Type.Literal('processing'), Type.Literal('completed'), Type.Literal('enqueued'),
14
+ ]);
15
+ const filterValue = Type.Union([text, Type.Array(text, { minItems: 1 })]);
16
+ const limitSchema = Type.Integer({ minimum: 1, maximum: Number.MAX_SAFE_INTEGER });
17
+ const listSchema = Type.Object({
18
+ status: Type.Optional(filterValue), taskType: Type.Optional(filterValue),
19
+ functionSlug: Type.Optional(text), dlq: Type.Optional(Type.Boolean()), limit: Type.Optional(limitSchema),
20
+ }, { additionalProperties: false });
21
+ const dlqSchema = Type.Object({ limit: Type.Optional(limitSchema) }, { additionalProperties: false });
22
+ const snapshotSchema = Type.Object({
23
+ id: idSchema, status: statusSchema, raw: Type.Unknown(),
24
+ progress: Type.Optional(Type.Union([Type.Number({ minimum: 0, maximum: 100 }), Type.Null()])),
25
+ error: Type.Optional(Type.Union([Type.String(), Type.Null()])),
26
+ updatedAt: Type.Optional(Type.Union([text, Type.Null()])),
27
+ }, { additionalProperties: false });
28
+ const liveParamsSchema = Type.Object({ taskId: idSchema }, { additionalProperties: false });
29
+ const eventSchema = Type.Object({
30
+ type: Type.Union([Type.Literal('INSERT'), Type.Literal('UPDATE'), Type.Literal('DELETE')]),
31
+ resource: text, payload: Type.Record(Type.String(), Type.Unknown()),
32
+ }, { additionalProperties: false });
33
+ const terminal = new Set<string>(['succeeded', 'failed', 'dead_lettered', 'cancelled', 'completed']);
34
+
35
+ export type SupaCloudTaskRecord = TaskRecord & { status: Static<typeof statusSchema> };
36
+ export type SupaCloudTaskListParams = Static<typeof listSchema>;
37
+ export type SupaCloudTaskDlqParams = Static<typeof dlqSchema>;
38
+
39
+ export interface SupaCloudTaskSdkSubmitOptions {
40
+ body?: Record<string, unknown>;
61
41
  headers?: Record<string, string>;
62
- retries?: number;
63
- timeoutSec?: number;
64
42
  idempotencyKey?: string;
65
- method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
66
- correlationId?: string;
67
- businessTaskId?: string;
68
43
  metadata?: Record<string, unknown>;
69
44
  }
70
45
 
71
- export interface SupaCloudTaskReceipt<
72
- TTask = SupaCloudTaskRecord,
73
- TUpdate = TTask,
74
- TWaitOptions = unknown,
75
- > {
76
- id?: string;
77
- taskId?: string;
78
- status?: string;
79
- wait(options?: TWaitOptions): Promise<TTask>;
80
- get?(): Promise<TTask>;
81
- cancel?(): Promise<unknown>;
82
- retry?(): Promise<unknown>;
83
- subscribe?(
84
- options: Omit<SupaCloudTaskSubscribeOptions<TUpdate>, 'onUpdate'> & {
85
- onUpdate: (task: TUpdate) => void;
86
- },
87
- ): TaskSubscription | (() => void);
46
+ export interface SupaCloudTaskSubscribeOptions {
47
+ onUpdate: (snapshot: unknown) => void;
48
+ onError: (error: unknown) => void;
88
49
  }
89
50
 
90
- /** Structural shape of `@supacloud/js` 0.23.x task clients. */
91
- export interface SupaCloudSdkTaskClient<
92
- TTask = SupaCloudSdkTaskRecord,
93
- TUpdate = SupaCloudTaskSnapshot,
94
- > {
95
- submit(
96
- taskName: string,
97
- options?: SupaCloudSdkTaskSubmitOptions,
98
- ): Promise<SupaCloudTaskReceipt<TTask, TUpdate, SupaCloudSdkTaskWaitOptions>>;
99
- get(taskId: string): Promise<TTask>;
100
- list?(params?: {
101
- status?: string | string[];
102
- taskType?: string | string[];
103
- functionSlug?: string;
104
- dlq?: boolean;
105
- limit?: number;
106
- }): Promise<TTask[]>;
107
- listDlq?(limit?: number): Promise<TTask[]>;
108
- cancel?(taskId: string): Promise<unknown>;
109
- retry?(taskId: string): Promise<unknown>;
110
- subscribe?(
111
- taskId: string,
112
- options: Omit<SupaCloudTaskSubscribeOptions<TUpdate>, 'onUpdate'> & {
113
- onUpdate: (task: TUpdate) => void;
114
- },
115
- ): TaskSubscription | (() => void);
116
- }
117
-
118
- /** Structural shape of a root `createSupaCloudClient()` task result. */
119
- export interface SupaCloudTaskSdkClient<
120
- TTask = SupaCloudTaskRecord,
121
- TUpdate = TTask,
122
- TWaitOptions = unknown,
123
- > {
51
+ /** SDK values stay unknown until their matching request/receipt is validated. */
52
+ export interface SupaCloudTaskClient {
124
53
  tasks: {
125
- submit(
126
- taskName: string,
127
- options?: SupaCloudSdkTaskSubmitOptions,
128
- ): Promise<SupaCloudTaskReceipt<TTask, TUpdate, TWaitOptions>>;
129
- get(taskId: string): Promise<TTask>;
130
- list?(params?: Record<string, unknown>): Promise<TTask[]>;
131
- listDlq?(limit?: number): Promise<TTask[]>;
132
- cancel?(taskId: string): Promise<unknown>;
133
- retry?(taskId: string): Promise<unknown>;
134
- subscribe?(
135
- taskId: string,
136
- options: Omit<SupaCloudTaskSubscribeOptions<TUpdate>, 'onUpdate'> & {
137
- onUpdate: (task: TUpdate) => void;
138
- },
139
- ): TaskSubscription | (() => void);
54
+ submit(name: string, options?: SupaCloudTaskSdkSubmitOptions): Promise<unknown>;
55
+ get(id: string): Promise<unknown>;
56
+ list(params?: SupaCloudTaskListParams): Promise<unknown>;
57
+ listDlq(limit?: number): Promise<unknown>;
58
+ cancel(id: string): Promise<unknown>;
59
+ retry(id: string): Promise<unknown>;
60
+ subscribe(id: string, options: SupaCloudTaskSubscribeOptions): unknown;
140
61
  };
141
62
  }
142
63
 
143
- type CurrentSupaCloudSdkRootClient = SupaCloudTaskSdkClient<
144
- SupaCloudSdkTaskRecord,
145
- SupaCloudTaskSnapshot,
146
- SupaCloudSdkTaskWaitOptions
147
- >;
148
-
149
- export interface SupaCloudTaskLegacyClient<TTask extends SupaCloudTaskRecord = SupaCloudTaskRecord> {
150
- submit(taskName: string, options?: SubmitTaskOptions): Promise<TaskHandle<TTask>>;
151
- get(taskId: string): Promise<TTask>;
152
- list?(params?: Record<string, unknown>): Promise<TTask[] | { data?: TTask[] }>;
153
- listDlq?(params?: Record<string, unknown>): Promise<TTask[] | { data?: TTask[] }>;
154
- cancel?(taskId: string): Promise<unknown>;
155
- retry?(taskId: string): Promise<unknown>;
156
- subscribe?(
157
- taskId: string,
158
- callback: (task: TTask) => void
159
- ): TaskSubscription | (() => void) | undefined;
160
- }
161
-
162
- /** Supported task-client shapes: the legacy root methods or a root SDK client. */
163
- export type SupaCloudTaskClient<TTask extends SupaCloudTaskRecord = SupaCloudTaskRecord> =
164
- | SupaCloudTaskLegacyClient<TTask>
165
- | SupaCloudTaskSdkClient<TTask>
166
- | CurrentSupaCloudSdkRootClient;
167
-
168
- interface LegacyTaskProviderOptions<TTask extends SupaCloudTaskRecord> {
169
- supacloud: SupaCloudTaskLegacyClient<TTask>;
170
- clientKind?: 'legacy';
171
- }
172
-
173
- interface SdkTaskProviderOptions<TTask extends SupaCloudTaskRecord> {
174
- supacloud:
175
- | SupaCloudSdkTaskClient
176
- | SupaCloudTaskSdkClient<TTask>
177
- | CurrentSupaCloudSdkRootClient;
178
- clientKind: 'sdk';
64
+ export interface SupaCloudTaskHandle extends TaskHandle<SupaCloudTaskRecord> {
65
+ id: string;
66
+ cancel(): Promise<SupaCloudTaskRecord>;
67
+ retry(): Promise<SupaCloudTaskRecord>;
68
+ subscribe(callback: (task: SupaCloudTaskRecord) => void, onError?: (error: TaskError) => void): () => void;
179
69
  }
180
70
 
181
- interface AutoSdkTaskProviderOptions<TTask extends SupaCloudTaskRecord> {
182
- supacloud: SupaCloudTaskSdkClient<TTask> | CurrentSupaCloudSdkRootClient;
183
- clientKind?: undefined;
71
+ export interface SupaCloudTaskProvider extends TaskProvider<SupaCloudTaskRecord> {
72
+ submit(name: string, options?: SubmitTaskOptions): Promise<SupaCloudTaskHandle>;
73
+ list(params?: SupaCloudTaskListParams): Promise<{ data: SupaCloudTaskRecord[]; total: number }>;
74
+ listDlq(params?: SupaCloudTaskDlqParams): Promise<{ data: SupaCloudTaskRecord[]; total: number }>;
75
+ cancel(id: string): Promise<SupaCloudTaskRecord>;
76
+ retry(id: string): Promise<SupaCloudTaskRecord>;
77
+ subscribe(id: string, callback: (task: SupaCloudTaskRecord) => void, onError?: (error: TaskError) => void): () => void;
184
78
  }
185
79
 
186
- export type CreateSupaCloudTaskProviderOptions<
187
- TTask extends SupaCloudTaskRecord = SupaCloudTaskRecord,
188
- > = LegacyTaskProviderOptions<TTask>
189
- | SdkTaskProviderOptions<TTask>
190
- | AutoSdkTaskProviderOptions<TTask>;
191
-
192
- interface LegacyTaskLiveProviderOptions<TTask extends SupaCloudTaskRecord> {
193
- supacloud: Pick<SupaCloudTaskLegacyClient<TTask>, 'subscribe'>;
194
- clientKind?: 'legacy';
195
- resource?: string;
196
- mapTaskToEvent?: (task: TTask, resource: string) => LiveEvent;
80
+ export interface CreateSupaCloudTaskProviderOptions {
81
+ supacloud: SupaCloudTaskClient;
82
+ onError?: (error: TaskError) => void;
197
83
  }
198
84
 
199
- interface SdkTaskLiveProviderOptions<TTask extends SupaCloudTaskRecord> {
200
- supacloud:
201
- | Pick<SupaCloudSdkTaskClient, 'subscribe'>
202
- | SupaCloudTaskSdkClient<TTask>
203
- | CurrentSupaCloudSdkRootClient;
204
- clientKind: 'sdk';
85
+ export interface CreateSupaCloudTaskLiveProviderOptions {
86
+ supacloud: { tasks: Pick<SupaCloudTaskClient['tasks'], 'subscribe'> };
205
87
  resource?: string;
206
- mapTaskToEvent?: (task: TTask, resource: string) => LiveEvent;
207
- }
208
-
209
- interface AutoSdkTaskLiveProviderOptions<TTask extends SupaCloudTaskRecord> {
210
- supacloud: SupaCloudTaskSdkClient<TTask> | CurrentSupaCloudSdkRootClient;
211
- clientKind?: undefined;
212
- resource?: string;
213
- mapTaskToEvent?: (task: TTask, resource: string) => LiveEvent;
214
- }
215
-
216
- export type CreateSupaCloudTaskLiveProviderOptions<
217
- TTask extends SupaCloudTaskRecord = SupaCloudTaskRecord,
218
- > = LegacyTaskLiveProviderOptions<TTask>
219
- | SdkTaskLiveProviderOptions<TTask>
220
- | AutoSdkTaskLiveProviderOptions<TTask>;
221
-
222
- function normalizeTaskSubscription(subscription: unknown): (() => void) | undefined {
223
- if (!subscription) return undefined;
224
- if (typeof subscription === 'function') return subscription as () => void;
225
- if (
226
- typeof subscription === 'object'
227
- && subscription !== null
228
- && 'unsubscribe' in subscription
229
- && typeof subscription.unsubscribe === 'function'
230
- ) {
231
- const unsubscribe = (subscription as { unsubscribe: () => void }).unsubscribe;
232
- return () => unsubscribe.call(subscription);
233
- }
234
- return undefined;
235
- }
236
-
237
- function readSdkTaskSubscription(subscription: unknown, operation: string): () => void {
238
- const unsubscribe = normalizeTaskSubscription(subscription);
239
- if (!unsubscribe) {
240
- throw invalidSdkResponse(operation, 'an invalid task subscription');
241
- }
242
- return unsubscribe;
88
+ mapTaskToEvent?: (task: SupaCloudTaskRecord, resource: string) => unknown;
89
+ onError?: (error: TaskError) => void;
243
90
  }
244
91
 
245
- function normalizeLegacyTaskList<TTask extends SupaCloudTaskRecord>(
246
- payload: unknown,
247
- context: string,
248
- ): TaskListResult<TTask> {
249
- if (Array.isArray(payload)) {
250
- return { data: payload as TTask[], total: payload.length };
92
+ function decode<S extends TSchema>(schema: S, value: unknown, input = false, write = false): Static<S> {
93
+ try {
94
+ const candidate = snapshotPlainData(value);
95
+ if (checkExact(schema, candidate)) return candidate;
96
+ } catch {
97
+ // Reflection and raw SDK errors must not escape this boundary.
251
98
  }
252
- if (isObjectRecord(payload) && Array.isArray(payload.data)) {
253
- return { data: payload.data as TTask[], total: payload.data.length };
254
- }
255
- throw new Error(`[svadmin/supabase] ${context} returned an invalid task list response`);
256
- }
257
-
258
- function invalidSdkResponse(operation: string, reason: string): TypeError {
259
- return new TypeError(`[svadmin/supabase] SupaCloud SDK ${operation} returned ${reason}`);
260
- }
261
-
262
- function isObjectRecord(payload: unknown): payload is Record<string, unknown> {
263
- return typeof payload === 'object' && payload !== null && !Array.isArray(payload);
264
- }
265
-
266
- function isSdkRootClient(
267
- payload: unknown,
268
- ): payload is SupaCloudTaskSdkClient {
269
- return isObjectRecord(payload) && isObjectRecord(payload.tasks);
270
- }
271
-
272
- interface ResolvedSdkTaskClient {
273
- submit(taskName: string, options?: SupaCloudSdkTaskSubmitOptions): Promise<unknown>;
274
- get(taskId: string): Promise<unknown>;
275
- list?(params?: Record<string, unknown>): Promise<unknown>;
276
- listDlq?(limit?: number): Promise<unknown>;
277
- cancel?(taskId: string): Promise<unknown>;
278
- retry?(taskId: string): Promise<unknown>;
279
- subscribe?(
280
- taskId: string,
281
- options: Omit<SupaCloudTaskSubscribeOptions<unknown>, 'onUpdate'> & {
282
- onUpdate: (task: unknown) => void;
283
- },
284
- ): unknown;
99
+ throw new TaskError(input ? 'INVALID_TASK_INPUT' : 'INVALID_TASK_RESPONSE', write);
285
100
  }
286
101
 
287
- function resolveSdkTaskClient(payload: unknown): ResolvedSdkTaskClient {
288
- const client = isSdkRootClient(payload) ? payload.tasks : payload;
289
- return client as ResolvedSdkTaskClient;
102
+ function taskRecord(value: unknown, id?: string, write = false): SupaCloudTaskRecord {
103
+ const task = decodeTaskRecord(value, id, write);
104
+ decode(idSchema, task.id, false, write);
105
+ return { ...task, status: decode(statusSchema, task.status, false, write) };
290
106
  }
291
107
 
292
- function nonEmptyString(candidate: unknown): string | undefined {
293
- return typeof candidate === 'string' && candidate.trim().length > 0 ? candidate : undefined;
294
- }
295
-
296
- function readSdkTaskId(
297
- taskPayload: Record<string, unknown>,
298
- rawTask: Record<string, unknown>,
299
- operation: string,
300
- ): string {
301
- const taskId = nonEmptyString(taskPayload.id) ?? nonEmptyString(rawTask.id);
302
- if (!taskId) throw invalidSdkResponse(operation, 'a task without a non-empty id');
303
- return taskId;
304
- }
305
-
306
- function readSdkTaskStatus(
307
- taskPayload: Record<string, unknown>,
308
- rawTask: Record<string, unknown>,
309
- operation: string,
310
- ): string {
311
- const taskStatus = nonEmptyString(taskPayload.status) ?? nonEmptyString(rawTask.status);
312
- if (!taskStatus) throw invalidSdkResponse(operation, 'a task without a non-empty status');
313
- return taskStatus;
314
- }
315
-
316
- function validateSdkProgress(
317
- taskPayload: Record<string, unknown>,
318
- operation: string,
319
- ): void {
320
- if (
321
- taskPayload.progress !== undefined
322
- && taskPayload.progress !== null
323
- && (typeof taskPayload.progress !== 'number' || !Number.isFinite(taskPayload.progress))
324
- ) {
325
- throw invalidSdkResponse(operation, 'a task with invalid progress');
108
+ function taskSnapshot(value: unknown, id: string): SupaCloudTaskRecord {
109
+ const snapshot = decode(snapshotSchema, value);
110
+ const task = taskRecord(snapshot.raw, id);
111
+ const expectedError = typeof task.error === 'string' ? task.error : task.error_message ?? null;
112
+ const expectedDate = task.updated_at ?? task.updatedAt ?? null;
113
+ if (snapshot.id !== task.id || snapshot.status !== task.status
114
+ || snapshot.progress !== undefined && snapshot.progress !== (task.progress ?? null)
115
+ || snapshot.error !== undefined && snapshot.error !== expectedError
116
+ || snapshot.updatedAt !== undefined && snapshot.updatedAt !== expectedDate) {
117
+ throw new TaskError('INVALID_TASK_RESPONSE');
326
118
  }
119
+ return task;
327
120
  }
328
121
 
329
- function validateSdkError(
330
- taskPayload: Record<string, unknown>,
331
- operation: string,
332
- ): void {
333
- const errorPayload = taskPayload.error ?? taskPayload.error_message;
334
- if (errorPayload !== undefined && errorPayload !== null && typeof errorPayload !== 'string') {
335
- throw invalidSdkResponse(operation, 'a task with invalid error');
336
- }
337
- }
338
-
339
- function validateSdkUpdatedAt(
340
- taskPayload: Record<string, unknown>,
341
- operation: string,
342
- ): void {
343
- const updatedAtPayload = taskPayload.updatedAt ?? taskPayload.updated_at;
344
- if (updatedAtPayload !== undefined && updatedAtPayload !== null && typeof updatedAtPayload !== 'string') {
345
- throw invalidSdkResponse(operation, 'a task with invalid updatedAt');
346
- }
347
- }
348
-
349
- function validateOptionalSdkTaskFields(
350
- taskPayload: Record<string, unknown>,
351
- operation: string,
352
- ): void {
353
- validateSdkProgress(taskPayload, operation);
354
- validateSdkError(taskPayload, operation);
355
- validateSdkUpdatedAt(taskPayload, operation);
356
- }
357
-
358
- function readSdkTask<TTask extends SupaCloudTaskRecord>(
359
- taskPayload: unknown,
360
- operation: string,
361
- ): TTask {
362
- if (!isObjectRecord(taskPayload)) {
363
- throw invalidSdkResponse(operation, 'a non-object task payload');
364
- }
365
- validateOptionalSdkTaskFields(taskPayload, operation);
366
-
367
- const rawTask = isObjectRecord(taskPayload.raw) ? taskPayload.raw : taskPayload;
368
- if (rawTask !== taskPayload) validateOptionalSdkTaskFields(rawTask, operation);
369
- const taskId = readSdkTaskId(taskPayload, rawTask, operation);
370
- const taskStatus = readSdkTaskStatus(taskPayload, rawTask, operation);
371
-
372
- const normalizedTask: Record<string, unknown> = { ...rawTask, id: taskId, status: taskStatus };
373
- const taskProgress = taskPayload.progress ?? rawTask.progress;
374
- if (typeof taskProgress === 'number') normalizedTask.progress = taskProgress;
375
- else delete normalizedTask.progress;
376
- if (taskPayload.error === null || typeof taskPayload.error === 'string') {
377
- normalizedTask.error = taskPayload.error;
378
- }
379
- if (taskPayload.updatedAt === null || typeof taskPayload.updatedAt === 'string') {
380
- normalizedTask.updatedAt = taskPayload.updatedAt;
381
- }
382
- return normalizedTask as TTask;
383
- }
384
-
385
- function readSdkTaskList<TTask extends SupaCloudTaskRecord>(
386
- taskListPayload: unknown,
387
- operation: string,
388
- ): TaskListResult<TTask> {
389
- if (!Array.isArray(taskListPayload)) {
390
- throw invalidSdkResponse(operation, 'a non-array task list');
391
- }
392
- const tasks = taskListPayload.map((task) => readSdkTask<TTask>(task, operation));
393
- return { data: tasks, total: tasks.length };
394
- }
395
-
396
- function assertOptionalReceiptMethod(
397
- receipt: Record<string, unknown>,
398
- method: 'cancel' | 'retry' | 'subscribe',
399
- ): void {
400
- if (receipt[method] !== undefined && typeof receipt[method] !== 'function') {
401
- throw invalidSdkResponse('submit', `a receipt with invalid ${method}`);
402
- }
403
- }
404
-
405
- function readSdkReceipt(receiptPayload: unknown): SupaCloudTaskReceipt {
406
- if (!isObjectRecord(receiptPayload)) {
407
- throw invalidSdkResponse('submit', 'a non-object receipt');
408
- }
409
- if (!nonEmptyString(receiptPayload.taskId) || typeof receiptPayload.wait !== 'function') {
410
- throw invalidSdkResponse('submit', 'a receipt without a non-empty taskId and wait method');
122
+ function ownField(value: unknown, key: string, write = false): unknown {
123
+ try {
124
+ if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
125
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
126
+ if (descriptor && 'value' in descriptor) {
127
+ const field: unknown = descriptor.value;
128
+ return field;
129
+ }
130
+ }
131
+ } catch {
132
+ // Never invoke receipt or subscription accessors.
411
133
  }
412
- assertOptionalReceiptMethod(receiptPayload, 'cancel');
413
- assertOptionalReceiptMethod(receiptPayload, 'retry');
414
- assertOptionalReceiptMethod(receiptPayload, 'subscribe');
415
- return receiptPayload as unknown as SupaCloudTaskReceipt;
134
+ throw new TaskError('INVALID_TASK_RESPONSE', write);
416
135
  }
417
136
 
418
- function adaptSdkReceipt<TTask extends SupaCloudTaskRecord>(
419
- receipt: SupaCloudTaskReceipt,
420
- ): TaskHandle<TTask> {
421
- const wait = receipt.wait;
422
- const taskHandle: TaskHandle<TTask> = {
423
- id: receipt.taskId,
424
- wait: async () => readSdkTask<TTask>(
425
- await wait.call(receipt),
426
- 'receipt.wait',
427
- ),
137
+ function method(value: unknown, key: string, write = false): (...args: unknown[]) => unknown {
138
+ const callable = ownField(value, key, write);
139
+ if (typeof callable !== 'function') throw new TaskError('INVALID_TASK_RESPONSE', write);
140
+ return (...args) => {
141
+ const result: unknown = Reflect.apply(callable, value, args);
142
+ return result;
428
143
  };
429
-
430
- const cancel = receipt.cancel;
431
- if (cancel) taskHandle.cancel = () => cancel.call(receipt);
432
- const retry = receipt.retry;
433
- if (retry) taskHandle.retry = () => retry.call(receipt);
434
- const subscribe = receipt.subscribe;
435
- if (subscribe) {
436
- taskHandle.subscribe = (callback) => readSdkTaskSubscription(
437
- subscribe.call(receipt, {
438
- onUpdate: (task) => callback(readSdkTask<TTask>(task, 'receipt.subscribe')),
439
- }),
440
- 'receipt.subscribe',
441
- );
442
- }
443
- return taskHandle;
444
144
  }
445
145
 
446
- function toSdkSubmitOptions(
447
- submitOptions?: SubmitTaskOptions,
448
- ): SupaCloudSdkTaskSubmitOptions | undefined {
449
- if (!submitOptions) return undefined;
450
- const { meta, ...sharedOptions } = submitOptions;
451
- return {
452
- ...sharedOptions,
453
- ...(meta === undefined ? {} : { metadata: meta }),
454
- };
146
+ async function request(invoke: () => unknown, write = false): Promise<unknown> {
147
+ try { return await invoke(); }
148
+ catch { throw new TaskError('TASK_PROVIDER_FAILED', write); }
455
149
  }
456
150
 
457
- function readSdkDlqLimit(params?: Record<string, unknown>): number | undefined {
458
- const limit = params?.limit;
459
- if (limit === undefined) return undefined;
460
- if (typeof limit !== 'number' || !Number.isFinite(limit)) {
461
- throw new TypeError('[svadmin/supabase] SupaCloud SDK tasks.listDlq requires a finite numeric limit');
462
- }
463
- return limit;
151
+ function listen(
152
+ start: (options: SupaCloudTaskSubscribeOptions) => unknown,
153
+ id: string,
154
+ callback: (task: SupaCloudTaskRecord) => void,
155
+ onError?: (error: TaskError) => void,
156
+ ): () => void {
157
+ return validatedTaskSubscription(
158
+ (onUpdate, onFailure) => start({ onUpdate, onError: onFailure }),
159
+ value => taskSnapshot(value, id), callback, onError,
160
+ );
464
161
  }
465
162
 
466
- function createLegacyTaskProvider<TTask extends SupaCloudTaskRecord>(
467
- supacloud: SupaCloudTaskLegacyClient<TTask>,
468
- ): TaskProvider<TTask> {
469
- return {
470
- submit: (taskName, submitOptions) => supacloud.submit(taskName, submitOptions),
471
- get: (taskId) => supacloud.get(taskId),
472
- async list(params) {
473
- if (!supacloud.list) {
474
- throw new Error('[svadmin/supabase] SupaCloud client does not implement tasks.list');
475
- }
476
- return normalizeLegacyTaskList(await supacloud.list(params), 'tasks.list');
477
- },
478
- async listDlq(params) {
479
- if (!supacloud.listDlq) {
480
- throw new Error('[svadmin/supabase] SupaCloud client does not implement tasks.listDlq');
163
+ function submitOptions(value: SubmitTaskOptions | undefined): SupaCloudTaskSdkSubmitOptions {
164
+ const input = decodeTaskSubmitOptions(value === undefined ? {} : value);
165
+ try {
166
+ if (input.headers) {
167
+ const headers = new Headers(input.headers);
168
+ if (headers.has('x-supacloud-task-metadata') || headers.has('x-supacloud-idempotency-key')) {
169
+ throw new TaskError('INVALID_TASK_INPUT');
481
170
  }
482
- return normalizeLegacyTaskList(await supacloud.listDlq(params), 'tasks.listDlq');
483
- },
484
- cancel(taskId) {
485
- if (!supacloud.cancel) {
486
- throw new Error('[svadmin/supabase] SupaCloud client does not implement tasks.cancel');
487
- }
488
- return supacloud.cancel(taskId);
489
- },
490
- retry(taskId) {
491
- if (!supacloud.retry) {
492
- throw new Error('[svadmin/supabase] SupaCloud client does not implement tasks.retry');
493
- }
494
- return supacloud.retry(taskId);
171
+ }
172
+ if (input.idempotencyKey !== undefined) {
173
+ if (!input.idempotencyKey.trim()) throw new TaskError('INVALID_TASK_INPUT');
174
+ new Headers({ 'x-supacloud-idempotency-key': input.idempotencyKey });
175
+ }
176
+ } catch { throw new TaskError('INVALID_TASK_INPUT'); }
177
+ return definedOptions({
178
+ body: input.body, headers: input.headers, idempotencyKey: input.idempotencyKey, metadata: input.meta,
179
+ });
180
+ }
181
+
182
+ function receipt(value: unknown, onError?: (error: TaskError) => void): SupaCloudTaskHandle {
183
+ const id = decode(idSchema, ownField(value, 'taskId', true), false, true);
184
+ decode(statusSchema, ownField(value, 'status', true), false, true);
185
+ const wait = method(value, 'wait', true);
186
+ const cancel = method(value, 'cancel', true);
187
+ const retry = method(value, 'retry', true);
188
+ const subscribe = method(value, 'subscribe', true);
189
+ return {
190
+ id,
191
+ async wait() {
192
+ const task = taskRecord(await request(() => wait()), id);
193
+ if (!terminal.has(task.status)) throw new TaskError('INVALID_TASK_RESPONSE');
194
+ return task;
495
195
  },
496
- subscribe(taskId, callback) {
497
- if (!supacloud.subscribe) {
498
- throw new Error('[svadmin/supabase] SupaCloud client does not implement tasks.subscribe');
499
- }
500
- return supacloud.subscribe(taskId, callback);
196
+ async cancel() { return taskRecord(await request(() => cancel(), true), id, true); },
197
+ async retry() { return taskRecord(await request(() => retry(), true), id, true); },
198
+ subscribe(callback, subscriptionError) {
199
+ return listen(options => subscribe(options), id, callback, subscriptionError ?? onError);
501
200
  },
502
201
  };
503
202
  }
504
203
 
505
- function createSdkTaskProvider<TTask extends SupaCloudTaskRecord>(
506
- supacloud: unknown,
507
- ): TaskProvider<TTask> {
508
- const sdk = resolveSdkTaskClient(supacloud);
204
+ export function createSupaCloudTaskProvider(options: CreateSupaCloudTaskProviderOptions): SupaCloudTaskProvider {
205
+ const sdk = taskClientField(taskClientField(options, 'supacloud'), 'tasks');
206
+ const submit = requiredTaskClientMethod(sdk, 'submit');
207
+ const get = requiredTaskClientMethod(sdk, 'get');
208
+ const list = requiredTaskClientMethod(sdk, 'list');
209
+ const listDlq = requiredTaskClientMethod(sdk, 'listDlq');
210
+ const cancel = requiredTaskClientMethod(sdk, 'cancel');
211
+ const retry = requiredTaskClientMethod(sdk, 'retry');
212
+ const subscribe = requiredTaskClientMethod(sdk, 'subscribe');
213
+ const onError = errorCallback(taskClientField(options, 'onError'));
214
+ const listResult = (value: unknown) => {
215
+ const values = decode(Type.Array(Type.Unknown()), value);
216
+ const data = values.map(value => taskRecord(value));
217
+ return { data, total: data.length };
218
+ };
509
219
  return {
510
- async submit(taskName, submitOptions) {
511
- const receipt = readSdkReceipt(await sdk.submit(
512
- taskName,
513
- toSdkSubmitOptions(submitOptions),
514
- ));
515
- return adaptSdkReceipt<TTask>(receipt);
220
+ async submit(name, options) {
221
+ const taskName = decode(nameSchema, name, true);
222
+ const params = submitOptions(options);
223
+ return receipt(await request(() => submit(taskName, params), true), onError);
516
224
  },
517
225
  async get(taskId) {
518
- return readSdkTask<TTask>(await sdk.get(taskId), 'tasks.get');
226
+ const id = decode(idSchema, taskId, true);
227
+ return taskRecord(await request(() => get(id)), id);
519
228
  },
520
229
  async list(params) {
521
- if (!sdk.list) {
522
- throw new Error('[svadmin/supabase] SupaCloud SDK client does not implement tasks.list');
523
- }
524
- return readSdkTaskList<TTask>(await sdk.list(params), 'tasks.list');
230
+ const input = decode(listSchema, params === undefined ? {} : params, true);
231
+ return listResult(await request(() => list(input)));
525
232
  },
526
233
  async listDlq(params) {
527
- if (!sdk.listDlq) {
528
- throw new Error('[svadmin/supabase] SupaCloud SDK client does not implement tasks.listDlq');
529
- }
530
- return readSdkTaskList<TTask>(
531
- await sdk.listDlq(readSdkDlqLimit(params)),
532
- 'tasks.listDlq',
533
- );
234
+ const input = decode(dlqSchema, params === undefined ? {} : params, true);
235
+ return listResult(await request(() => listDlq(input.limit)));
534
236
  },
535
- cancel(taskId) {
536
- if (!sdk.cancel) {
537
- throw new Error('[svadmin/supabase] SupaCloud SDK client does not implement tasks.cancel');
538
- }
539
- return sdk.cancel(taskId);
237
+ async cancel(taskId) {
238
+ const id = decode(idSchema, taskId, true);
239
+ return taskRecord(await request(() => cancel(id), true), id, true);
540
240
  },
541
- retry(taskId) {
542
- if (!sdk.retry) {
543
- throw new Error('[svadmin/supabase] SupaCloud SDK client does not implement tasks.retry');
544
- }
545
- return sdk.retry(taskId);
241
+ async retry(taskId) {
242
+ const id = decode(idSchema, taskId, true);
243
+ return taskRecord(await request(() => retry(id), true), id, true);
546
244
  },
547
- subscribe(taskId, callback) {
548
- if (!sdk.subscribe) {
549
- throw new Error('[svadmin/supabase] SupaCloud SDK client does not implement tasks.subscribe');
550
- }
551
- return readSdkTaskSubscription(
552
- sdk.subscribe(taskId, {
553
- onUpdate: (task) => callback(readSdkTask<TTask>(task, 'tasks.subscribe')),
554
- }),
555
- 'tasks.subscribe',
556
- );
245
+ subscribe(taskId, callback, subscriptionError) {
246
+ const id = decode(idSchema, taskId, true);
247
+ return listen(options => subscribe(id, options), id, callback, subscriptionError ?? onError);
557
248
  },
558
249
  };
559
250
  }
560
251
 
561
- function defaultMapTaskToEvent<TTask extends SupaCloudTaskRecord>(
562
- task: TTask,
563
- resource: string,
564
- ): LiveEvent {
565
- return {
566
- type: 'UPDATE',
567
- resource,
568
- payload: task as Record<string, unknown>,
569
- };
570
- }
571
-
572
- function requiredLiveTaskId(liveParams?: Record<string, unknown>): string {
573
- const taskId = nonEmptyString(liveParams?.taskId);
574
- if (!taskId) {
575
- throw new Error('[svadmin/supabase] createSupaCloudTaskLiveProvider requires liveParams.taskId');
576
- }
577
- return taskId;
578
- }
579
-
580
- function createLegacyTaskLiveProvider<TTask extends SupaCloudTaskRecord>(
581
- options: LegacyTaskLiveProviderOptions<TTask>,
582
- ): LiveProvider {
583
- const resourceName = options.resource ?? 'tasks';
584
- const mapTaskToEvent = options.mapTaskToEvent ?? defaultMapTaskToEvent<TTask>;
585
- return {
586
- subscribe({ resource, liveParams, callback }) {
587
- if (!options.supacloud.subscribe) {
588
- throw new Error('[svadmin/supabase] SupaCloud client does not implement tasks.subscribe');
589
- }
590
- const taskId = requiredLiveTaskId(liveParams);
591
- const subscription = options.supacloud.subscribe(taskId, (task) => {
592
- callback(mapTaskToEvent(task, resource || resourceName));
593
- });
594
- return normalizeTaskSubscription(subscription) ?? (() => {});
595
- },
596
- };
252
+ function errorCallback(value: unknown): ((error: TaskError) => unknown) | undefined {
253
+ if (value === undefined) return undefined;
254
+ if (typeof value !== 'function') throw new TaskError('INVALID_TASK_INPUT');
255
+ return error => { const result: unknown = Reflect.apply(value, undefined, [error]); return result; };
597
256
  }
598
257
 
599
- function createSdkTaskLiveProvider<TTask extends SupaCloudTaskRecord>(
600
- options: {
601
- supacloud: unknown;
602
- resource?: string;
603
- mapTaskToEvent?: (task: TTask, resource: string) => LiveEvent;
604
- },
605
- ): LiveProvider {
606
- const sdk = resolveSdkTaskClient(options.supacloud);
607
- const resourceName = options.resource ?? 'tasks';
608
- const mapTaskToEvent = options.mapTaskToEvent ?? defaultMapTaskToEvent<TTask>;
258
+ export function createSupaCloudTaskLiveProvider(options: CreateSupaCloudTaskLiveProviderOptions): LiveProvider {
259
+ const sdk = taskClientField(taskClientField(options, 'supacloud'), 'tasks');
260
+ const subscribe = requiredTaskClientMethod(sdk, 'subscribe');
261
+ const resource = taskClientField(options, 'resource');
262
+ const resourceName = decode(text, resource === undefined ? 'tasks' : resource, true);
263
+ const map = taskClientField(options, 'mapTaskToEvent');
264
+ if (map !== undefined && typeof map !== 'function') throw new TaskError('INVALID_TASK_INPUT');
265
+ const onError = errorCallback(taskClientField(options, 'onError'));
609
266
  return {
610
267
  subscribe({ resource, liveParams, callback }) {
611
- if (!sdk.subscribe) {
612
- throw new Error('[svadmin/supabase] SupaCloud SDK client does not implement tasks.subscribe');
613
- }
614
- const taskId = requiredLiveTaskId(liveParams);
615
- return readSdkTaskSubscription(
616
- sdk.subscribe(taskId, {
617
- onUpdate: (task) => callback(mapTaskToEvent(
618
- readSdkTask<TTask>(task, 'tasks.subscribe'),
619
- resource || resourceName,
620
- )),
621
- }),
622
- 'tasks.subscribe',
268
+ const resourceId = decode(text, resource === undefined ? resourceName : resource, true);
269
+ const { taskId } = decode(liveParamsSchema, liveParams, true);
270
+ return validatedTaskSubscription(
271
+ (onUpdate, onError) => subscribe(taskId, { onUpdate, onError }),
272
+ value => {
273
+ const task = taskSnapshot(value, taskId);
274
+ let mapped: unknown;
275
+ try {
276
+ mapped = map ? Reflect.apply(map, undefined, [task, resourceId]) : { type: 'UPDATE', resource: resourceId, payload: task };
277
+ } catch { throw new TaskError('TASK_CALLBACK_FAILED'); }
278
+ const event: LiveEvent = decode(eventSchema, mapped);
279
+ if (event.resource !== resourceId) throw new TaskError('INVALID_TASK_RESPONSE');
280
+ return event;
281
+ },
282
+ callback, onError,
623
283
  );
624
284
  },
625
285
  };
626
286
  }
627
-
628
- export function createSupaCloudTaskProvider<
629
- TTask extends SupaCloudTaskRecord = SupaCloudTaskRecord,
630
- >(options: CreateSupaCloudTaskProviderOptions<TTask>): TaskProvider<TTask> {
631
- if (options.clientKind === 'legacy') {
632
- return createLegacyTaskProvider(options.supacloud);
633
- }
634
- if (options.clientKind === 'sdk') {
635
- return createSdkTaskProvider<TTask>(options.supacloud);
636
- }
637
- if (isSdkRootClient(options.supacloud)) {
638
- return createSdkTaskProvider<TTask>(options.supacloud);
639
- }
640
- return createLegacyTaskProvider(options.supacloud as SupaCloudTaskLegacyClient<TTask>);
641
- }
642
-
643
- export function createSupaCloudTaskLiveProvider<
644
- TTask extends SupaCloudTaskRecord = SupaCloudTaskRecord,
645
- >(options: CreateSupaCloudTaskLiveProviderOptions<TTask>): LiveProvider {
646
- if (options.clientKind === 'legacy') {
647
- return createLegacyTaskLiveProvider(options);
648
- }
649
- if (options.clientKind === 'sdk') {
650
- return createSdkTaskLiveProvider(options);
651
- }
652
- if (isSdkRootClient(options.supacloud)) {
653
- return createSdkTaskLiveProvider({
654
- supacloud: options.supacloud,
655
- resource: options.resource,
656
- mapTaskToEvent: options.mapTaskToEvent,
657
- });
658
- }
659
- return createLegacyTaskLiveProvider(options as LegacyTaskLiveProviderOptions<TTask>);
660
- }