@syncular/core 0.0.1-100

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.
Files changed (99) hide show
  1. package/dist/blobs.d.ts +146 -0
  2. package/dist/blobs.d.ts.map +1 -0
  3. package/dist/blobs.js +47 -0
  4. package/dist/blobs.js.map +1 -0
  5. package/dist/conflict.d.ts +22 -0
  6. package/dist/conflict.d.ts.map +1 -0
  7. package/dist/conflict.js +81 -0
  8. package/dist/conflict.js.map +1 -0
  9. package/dist/index.d.ts +24 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +36 -0
  12. package/dist/index.js.map +1 -0
  13. package/dist/kysely-serialize.d.ts +22 -0
  14. package/dist/kysely-serialize.d.ts.map +1 -0
  15. package/dist/kysely-serialize.js +147 -0
  16. package/dist/kysely-serialize.js.map +1 -0
  17. package/dist/logger.d.ts +29 -0
  18. package/dist/logger.d.ts.map +1 -0
  19. package/dist/logger.js +26 -0
  20. package/dist/logger.js.map +1 -0
  21. package/dist/proxy/index.d.ts +5 -0
  22. package/dist/proxy/index.d.ts.map +1 -0
  23. package/dist/proxy/index.js +5 -0
  24. package/dist/proxy/index.js.map +1 -0
  25. package/dist/proxy/types.d.ts +54 -0
  26. package/dist/proxy/types.d.ts.map +1 -0
  27. package/dist/proxy/types.js +7 -0
  28. package/dist/proxy/types.js.map +1 -0
  29. package/dist/schemas/blobs.d.ts +76 -0
  30. package/dist/schemas/blobs.d.ts.map +1 -0
  31. package/dist/schemas/blobs.js +63 -0
  32. package/dist/schemas/blobs.js.map +1 -0
  33. package/dist/schemas/common.d.ts +28 -0
  34. package/dist/schemas/common.d.ts.map +1 -0
  35. package/dist/schemas/common.js +26 -0
  36. package/dist/schemas/common.js.map +1 -0
  37. package/dist/schemas/index.d.ts +7 -0
  38. package/dist/schemas/index.d.ts.map +1 -0
  39. package/dist/schemas/index.js +7 -0
  40. package/dist/schemas/index.js.map +1 -0
  41. package/dist/schemas/sync.d.ts +391 -0
  42. package/dist/schemas/sync.d.ts.map +1 -0
  43. package/dist/schemas/sync.js +157 -0
  44. package/dist/schemas/sync.js.map +1 -0
  45. package/dist/scopes/index.d.ts +65 -0
  46. package/dist/scopes/index.d.ts.map +1 -0
  47. package/dist/scopes/index.js +67 -0
  48. package/dist/scopes/index.js.map +1 -0
  49. package/dist/snapshot-chunks.d.ts +26 -0
  50. package/dist/snapshot-chunks.d.ts.map +1 -0
  51. package/dist/snapshot-chunks.js +89 -0
  52. package/dist/snapshot-chunks.js.map +1 -0
  53. package/dist/telemetry.d.ts +114 -0
  54. package/dist/telemetry.d.ts.map +1 -0
  55. package/dist/telemetry.js +113 -0
  56. package/dist/telemetry.js.map +1 -0
  57. package/dist/transforms.d.ts +146 -0
  58. package/dist/transforms.d.ts.map +1 -0
  59. package/dist/transforms.js +155 -0
  60. package/dist/transforms.js.map +1 -0
  61. package/dist/types.d.ts +129 -0
  62. package/dist/types.d.ts.map +1 -0
  63. package/dist/types.js +20 -0
  64. package/dist/types.js.map +1 -0
  65. package/dist/utils/id.d.ts +2 -0
  66. package/dist/utils/id.d.ts.map +1 -0
  67. package/dist/utils/id.js +8 -0
  68. package/dist/utils/id.js.map +1 -0
  69. package/dist/utils/index.d.ts +3 -0
  70. package/dist/utils/index.d.ts.map +1 -0
  71. package/dist/utils/index.js +3 -0
  72. package/dist/utils/index.js.map +1 -0
  73. package/dist/utils/object.d.ts +2 -0
  74. package/dist/utils/object.d.ts.map +1 -0
  75. package/dist/utils/object.js +4 -0
  76. package/dist/utils/object.js.map +1 -0
  77. package/package.json +57 -0
  78. package/src/__tests__/conflict.test.ts +325 -0
  79. package/src/__tests__/telemetry.test.ts +170 -0
  80. package/src/__tests__/utils.test.ts +27 -0
  81. package/src/blobs.ts +202 -0
  82. package/src/conflict.ts +92 -0
  83. package/src/index.ts +36 -0
  84. package/src/kysely-serialize.ts +214 -0
  85. package/src/logger.ts +38 -0
  86. package/src/proxy/index.ts +10 -0
  87. package/src/proxy/types.ts +57 -0
  88. package/src/schemas/blobs.ts +101 -0
  89. package/src/schemas/common.ts +45 -0
  90. package/src/schemas/index.ts +7 -0
  91. package/src/schemas/sync.ts +226 -0
  92. package/src/scopes/index.ts +122 -0
  93. package/src/snapshot-chunks.ts +112 -0
  94. package/src/telemetry.ts +238 -0
  95. package/src/transforms.ts +256 -0
  96. package/src/types.ts +158 -0
  97. package/src/utils/id.ts +7 -0
  98. package/src/utils/index.ts +2 -0
  99. package/src/utils/object.ts +3 -0
@@ -0,0 +1,238 @@
1
+ /**
2
+ * @syncular/core - Runtime telemetry abstraction
3
+ *
4
+ * Provides vendor-neutral logging, tracing, and metrics interfaces so
5
+ * Syncular libraries can emit telemetry without coupling to a specific SDK.
6
+ */
7
+
8
+ /**
9
+ * Supported log levels.
10
+ */
11
+ export type SyncTelemetryLevel =
12
+ | 'trace'
13
+ | 'debug'
14
+ | 'info'
15
+ | 'warn'
16
+ | 'error'
17
+ | 'fatal';
18
+
19
+ /**
20
+ * Primitive attribute value used by traces and metrics.
21
+ */
22
+ export type SyncTelemetryAttributeValue = string | number | boolean;
23
+
24
+ /**
25
+ * Attribute bag used by traces and metrics.
26
+ */
27
+ export type SyncTelemetryAttributes = Record<
28
+ string,
29
+ SyncTelemetryAttributeValue
30
+ >;
31
+
32
+ /**
33
+ * Structured sync log event.
34
+ */
35
+ export interface SyncTelemetryEvent {
36
+ event: string;
37
+ level?: SyncTelemetryLevel;
38
+ userId?: string;
39
+ durationMs?: number;
40
+ rowCount?: number;
41
+ resetRequired?: boolean;
42
+ error?: string;
43
+ [key: string]: unknown;
44
+ }
45
+
46
+ /**
47
+ * Span creation options.
48
+ */
49
+ export interface SyncSpanOptions {
50
+ name: string;
51
+ op?: string;
52
+ attributes?: SyncTelemetryAttributes;
53
+ }
54
+
55
+ /**
56
+ * Span API exposed to Syncular internals.
57
+ */
58
+ export interface SyncSpan {
59
+ setAttribute(name: string, value: SyncTelemetryAttributeValue): void;
60
+ setAttributes(attributes: SyncTelemetryAttributes): void;
61
+ setStatus(status: 'ok' | 'error'): void;
62
+ }
63
+
64
+ /**
65
+ * Tracing interface.
66
+ */
67
+ export interface SyncTracer {
68
+ startSpan<T>(options: SyncSpanOptions, callback: (span: SyncSpan) => T): T;
69
+ }
70
+
71
+ /**
72
+ * Metric record options.
73
+ */
74
+ export interface SyncMetricOptions {
75
+ attributes?: SyncTelemetryAttributes;
76
+ unit?: string;
77
+ }
78
+
79
+ /**
80
+ * Metrics interface.
81
+ */
82
+ export interface SyncMetrics {
83
+ count(name: string, value?: number, options?: SyncMetricOptions): void;
84
+ gauge(name: string, value: number, options?: SyncMetricOptions): void;
85
+ distribution(name: string, value: number, options?: SyncMetricOptions): void;
86
+ }
87
+
88
+ /**
89
+ * Unified telemetry interface.
90
+ */
91
+ export interface SyncTelemetry {
92
+ log(event: SyncTelemetryEvent): void;
93
+ tracer: SyncTracer;
94
+ metrics: SyncMetrics;
95
+ captureException(error: unknown, context?: Record<string, unknown>): void;
96
+ }
97
+
98
+ const noopSpan: SyncSpan = {
99
+ setAttribute() {},
100
+ setAttributes() {},
101
+ setStatus() {},
102
+ };
103
+
104
+ const noopTracer: SyncTracer = {
105
+ startSpan(_options, callback) {
106
+ return callback(noopSpan);
107
+ },
108
+ };
109
+
110
+ const noopMetrics: SyncMetrics = {
111
+ count() {},
112
+ gauge() {},
113
+ distribution() {},
114
+ };
115
+
116
+ function createConsoleLogger(): (event: SyncTelemetryEvent) => void {
117
+ const isNode =
118
+ typeof globalThis !== 'undefined' &&
119
+ typeof globalThis.setImmediate === 'function';
120
+
121
+ const defer = isNode
122
+ ? (fn: () => void) => globalThis.setImmediate(fn)
123
+ : (fn: () => void) => setTimeout(fn, 0);
124
+
125
+ return (event: SyncTelemetryEvent) => {
126
+ defer(() => {
127
+ const level = event.level ?? (event.error ? 'error' : 'info');
128
+ const payload = {
129
+ timestamp: new Date().toISOString(),
130
+ level,
131
+ ...event,
132
+ };
133
+ console.log(JSON.stringify(payload));
134
+ });
135
+ };
136
+ }
137
+
138
+ /**
139
+ * Create console-backed default telemetry (logs only; no-op tracing/metrics).
140
+ */
141
+ export function createDefaultSyncTelemetry(): SyncTelemetry {
142
+ const logger = createConsoleLogger();
143
+ return {
144
+ log(event) {
145
+ logger(event);
146
+ },
147
+ tracer: noopTracer,
148
+ metrics: noopMetrics,
149
+ captureException(error, context) {
150
+ const message =
151
+ error instanceof Error
152
+ ? error.message
153
+ : `Unknown error: ${String(error)}`;
154
+ logger({
155
+ event: 'sync.exception',
156
+ level: 'error',
157
+ error: message,
158
+ ...(context ?? {}),
159
+ });
160
+ },
161
+ };
162
+ }
163
+
164
+ let activeSyncTelemetry: SyncTelemetry = createDefaultSyncTelemetry();
165
+
166
+ /**
167
+ * Get currently configured telemetry backend.
168
+ */
169
+ export function getSyncTelemetry(): SyncTelemetry {
170
+ return activeSyncTelemetry;
171
+ }
172
+
173
+ /**
174
+ * Replace active telemetry backend.
175
+ */
176
+ export function configureSyncTelemetry(telemetry: SyncTelemetry): void {
177
+ activeSyncTelemetry = telemetry;
178
+ }
179
+
180
+ /**
181
+ * Reset telemetry backend to default console implementation.
182
+ */
183
+ export function resetSyncTelemetry(): void {
184
+ activeSyncTelemetry = createDefaultSyncTelemetry();
185
+ }
186
+
187
+ /**
188
+ * Capture an exception through the active telemetry backend.
189
+ */
190
+ export function captureSyncException(
191
+ error: unknown,
192
+ context?: Record<string, unknown>
193
+ ): void {
194
+ activeSyncTelemetry.captureException(error, context);
195
+ }
196
+
197
+ /**
198
+ * Start a span through the active telemetry backend.
199
+ */
200
+ export function startSyncSpan<T>(
201
+ options: SyncSpanOptions,
202
+ callback: (span: SyncSpan) => T
203
+ ): T {
204
+ return activeSyncTelemetry.tracer.startSpan(options, callback);
205
+ }
206
+
207
+ /**
208
+ * Record a counter metric through the active telemetry backend.
209
+ */
210
+ export function countSyncMetric(
211
+ name: string,
212
+ value?: number,
213
+ options?: SyncMetricOptions
214
+ ): void {
215
+ activeSyncTelemetry.metrics.count(name, value, options);
216
+ }
217
+
218
+ /**
219
+ * Record a gauge metric through the active telemetry backend.
220
+ */
221
+ export function gaugeSyncMetric(
222
+ name: string,
223
+ value: number,
224
+ options?: SyncMetricOptions
225
+ ): void {
226
+ activeSyncTelemetry.metrics.gauge(name, value, options);
227
+ }
228
+
229
+ /**
230
+ * Record a distribution metric through the active telemetry backend.
231
+ */
232
+ export function distributionSyncMetric(
233
+ name: string,
234
+ value: number,
235
+ options?: SyncMetricOptions
236
+ ): void {
237
+ activeSyncTelemetry.metrics.distribution(name, value, options);
238
+ }
@@ -0,0 +1,256 @@
1
+ /**
2
+ * @syncular/core - Data transformation hooks
3
+ *
4
+ * Provides interfaces for field-level transformations (e.g., encryption/decryption)
5
+ * that can be applied during sync operations.
6
+ */
7
+
8
+ /**
9
+ * Direction of the transformation.
10
+ * - 'toClient': Server → Client (e.g., decrypt for client)
11
+ * - 'toServer': Client → Server (e.g., encrypt for server)
12
+ */
13
+ export type TransformDirection = 'toClient' | 'toServer';
14
+
15
+ /**
16
+ * Context passed to transform functions.
17
+ */
18
+ export interface TransformContext {
19
+ /** Direction of transformation */
20
+ direction: TransformDirection;
21
+ /** Scope name */
22
+ scope: string;
23
+ /** Table name */
24
+ table: string;
25
+ /** Row ID */
26
+ rowId: string;
27
+ /** User ID performing the operation */
28
+ userId: string;
29
+ }
30
+
31
+ /**
32
+ * A field transformer handles transformation of a single field.
33
+ *
34
+ * @example
35
+ * const secretNotesTransformer: FieldTransformer = {
36
+ * field: 'secret_notes',
37
+ * async transform(value, ctx) {
38
+ * const key = await getUserEncryptionKey(ctx.userId);
39
+ * return ctx.direction === 'toClient'
40
+ * ? decrypt(value as string, key)
41
+ * : encrypt(value as string, key);
42
+ * }
43
+ * };
44
+ */
45
+ export interface FieldTransformer {
46
+ /** Field name to transform */
47
+ field: string;
48
+ /**
49
+ * Transform the field value.
50
+ * @param value - Current field value
51
+ * @param ctx - Transform context
52
+ * @returns Transformed value
53
+ */
54
+ transform(value: unknown, ctx: TransformContext): Promise<unknown> | unknown;
55
+ }
56
+
57
+ /**
58
+ * Configuration for transforms on a scope.
59
+ */
60
+ export interface ScopeTransformConfig {
61
+ /** Scope name this config applies to */
62
+ scope: string;
63
+ /** Field transformers for this scope */
64
+ fields?: FieldTransformer[];
65
+ }
66
+
67
+ /**
68
+ * Registry for managing data transforms.
69
+ *
70
+ * @example
71
+ * const transforms = new TransformRegistry();
72
+ *
73
+ * transforms.register({
74
+ * scope: 'tasks',
75
+ * fields: [{
76
+ * field: 'secret_notes',
77
+ * async transform(value, ctx) {
78
+ * const key = await getUserEncryptionKey(ctx.userId);
79
+ * return ctx.direction === 'toClient'
80
+ * ? decrypt(value as string, key)
81
+ * : encrypt(value as string, key);
82
+ * }
83
+ * }]
84
+ * });
85
+ *
86
+ * // Apply transforms to data
87
+ * const transformed = await transforms.apply(
88
+ * [{ id: '1', secret_notes: 'encrypted...' }],
89
+ * { direction: 'toClient', scope: 'tasks', ... }
90
+ * );
91
+ */
92
+ export class TransformRegistry {
93
+ private configs: Map<string, ScopeTransformConfig> = new Map();
94
+
95
+ /**
96
+ * Register transform config for a scope.
97
+ * @throws If config for this scope is already registered
98
+ */
99
+ register(config: ScopeTransformConfig): void {
100
+ if (this.configs.has(config.scope)) {
101
+ throw new Error(
102
+ `Transform config for scope "${config.scope}" is already registered`
103
+ );
104
+ }
105
+ this.configs.set(config.scope, config);
106
+ }
107
+
108
+ /**
109
+ * Unregister transform config by scope.
110
+ * @returns true if config was found and removed
111
+ */
112
+ unregister(scope: string): boolean {
113
+ return this.configs.delete(scope);
114
+ }
115
+
116
+ /**
117
+ * Get config for a scope.
118
+ */
119
+ get(scope: string): ScopeTransformConfig | undefined {
120
+ return this.configs.get(scope);
121
+ }
122
+
123
+ /**
124
+ * Check if any transforms are registered for a scope.
125
+ */
126
+ hasTransforms(scope: string): boolean {
127
+ const config = this.configs.get(scope);
128
+ return config !== undefined && (config.fields?.length ?? 0) > 0;
129
+ }
130
+
131
+ /**
132
+ * Get all registered configs.
133
+ */
134
+ getAll(): ScopeTransformConfig[] {
135
+ return Array.from(this.configs.values());
136
+ }
137
+
138
+ /**
139
+ * Apply transforms to a single row.
140
+ *
141
+ * @param row - Row data to transform
142
+ * @param ctx - Transform context (without rowId, will be extracted from row)
143
+ * @param rowIdField - Field name for row ID (default: 'id')
144
+ * @returns Transformed row
145
+ */
146
+ async applyToRow<T extends Record<string, unknown>>(
147
+ row: T,
148
+ ctx: Omit<TransformContext, 'rowId'>,
149
+ rowIdField = 'id'
150
+ ): Promise<T> {
151
+ const config = this.configs.get(ctx.scope);
152
+ if (!config?.fields?.length) {
153
+ return row;
154
+ }
155
+
156
+ const rowId = String(row[rowIdField] ?? '');
157
+ const fullCtx: TransformContext = { ...ctx, rowId };
158
+ const result = { ...row };
159
+
160
+ for (const transformer of config.fields) {
161
+ if (transformer.field in result) {
162
+ try {
163
+ result[transformer.field as keyof T] = (await transformer.transform(
164
+ result[transformer.field],
165
+ fullCtx
166
+ )) as T[keyof T];
167
+ } catch (err) {
168
+ console.error(
169
+ `[transforms] Error transforming field "${transformer.field}" for ${ctx.scope}:${rowId}:`,
170
+ err
171
+ );
172
+ // Keep original value on error
173
+ }
174
+ }
175
+ }
176
+
177
+ return result;
178
+ }
179
+
180
+ /**
181
+ * Apply transforms to multiple rows.
182
+ *
183
+ * @param rows - Array of rows to transform
184
+ * @param ctx - Transform context (without rowId)
185
+ * @param rowIdField - Field name for row ID (default: 'id')
186
+ * @returns Transformed rows
187
+ */
188
+ async apply<T extends Record<string, unknown>>(
189
+ rows: T[],
190
+ ctx: Omit<TransformContext, 'rowId'>,
191
+ rowIdField = 'id'
192
+ ): Promise<T[]> {
193
+ const config = this.configs.get(ctx.scope);
194
+ if (!config?.fields?.length) {
195
+ return rows;
196
+ }
197
+
198
+ return Promise.all(
199
+ rows.map((row) => this.applyToRow(row, ctx, rowIdField))
200
+ );
201
+ }
202
+
203
+ /**
204
+ * Apply transforms to a mutation payload.
205
+ *
206
+ * @param payload - Mutation payload (may be partial row)
207
+ * @param ctx - Full transform context
208
+ * @returns Transformed payload
209
+ */
210
+ async applyToPayload<T extends Record<string, unknown>>(
211
+ payload: T | null,
212
+ ctx: TransformContext
213
+ ): Promise<T | null> {
214
+ if (!payload) return null;
215
+
216
+ const config = this.configs.get(ctx.scope);
217
+ if (!config?.fields?.length) {
218
+ return payload;
219
+ }
220
+
221
+ const result = { ...payload };
222
+
223
+ for (const transformer of config.fields) {
224
+ if (transformer.field in result) {
225
+ try {
226
+ result[transformer.field as keyof T] = (await transformer.transform(
227
+ result[transformer.field],
228
+ ctx
229
+ )) as T[keyof T];
230
+ } catch (err) {
231
+ console.error(
232
+ `[transforms] Error transforming field "${transformer.field}" for ${ctx.scope}:${ctx.rowId}:`,
233
+ err
234
+ );
235
+ // Keep original value on error
236
+ }
237
+ }
238
+ }
239
+
240
+ return result;
241
+ }
242
+
243
+ /**
244
+ * Clear all registered configs.
245
+ */
246
+ clear(): void {
247
+ this.configs.clear();
248
+ }
249
+ }
250
+
251
+ /**
252
+ * Create a new transform registry.
253
+ */
254
+ export function createTransformRegistry(): TransformRegistry {
255
+ return new TransformRegistry();
256
+ }
package/src/types.ts ADDED
@@ -0,0 +1,158 @@
1
+ /**
2
+ * @syncular/core - Shared types for sync infrastructure
3
+ *
4
+ * Non-protocol types: conflict detection, transport interfaces.
5
+ * Protocol types (SyncOp, SyncPushRequest, etc.) live in ./schemas/sync.ts
6
+ */
7
+
8
+ import type { SyncCombinedRequest, SyncCombinedResponse } from './schemas/sync';
9
+
10
+ // ============================================================================
11
+ // Conflict Detection Types
12
+ // ============================================================================
13
+
14
+ /**
15
+ * Result of a conflict check - no conflict
16
+ */
17
+ interface ConflictCheckResultOk {
18
+ hasConflict: false;
19
+ }
20
+
21
+ /**
22
+ * Result of a conflict check - conflict detected
23
+ */
24
+ interface ConflictCheckResultConflict {
25
+ hasConflict: true;
26
+ /** Fields with conflicting changes */
27
+ conflictingFields: string[];
28
+ /** Current server row state */
29
+ serverRow: Record<string, unknown>;
30
+ /** Current server version */
31
+ serverVersion: number;
32
+ }
33
+
34
+ /**
35
+ * Union type for conflict check results
36
+ */
37
+ export type ConflictCheckResult =
38
+ | ConflictCheckResultOk
39
+ | ConflictCheckResultConflict;
40
+
41
+ /**
42
+ * Result of a field-level merge - can merge
43
+ */
44
+ export interface MergeResultOk {
45
+ canMerge: true;
46
+ /** Merged payload combining client and server changes */
47
+ mergedPayload: Record<string, unknown>;
48
+ }
49
+
50
+ /**
51
+ * Result of a field-level merge - cannot merge
52
+ */
53
+ export interface MergeResultConflict {
54
+ canMerge: false;
55
+ /** Fields that cannot be auto-merged */
56
+ conflictingFields: string[];
57
+ }
58
+
59
+ /**
60
+ * Union type for merge results
61
+ */
62
+ export type MergeResult = MergeResultOk | MergeResultConflict;
63
+
64
+ // ============================================================================
65
+ // Transport Types
66
+ // ============================================================================
67
+
68
+ /**
69
+ * Options for transport operations.
70
+ * Provides hooks for auth errors and cancellation support.
71
+ */
72
+ export interface SyncTransportOptions {
73
+ /**
74
+ * Called when auth fails (401/403).
75
+ * Return true to retry the request after refreshing auth.
76
+ */
77
+ onAuthError?: () => Promise<boolean>;
78
+ /**
79
+ * Abort signal for cancellation support.
80
+ */
81
+ signal?: AbortSignal;
82
+ }
83
+
84
+ /**
85
+ * Blob transport operations (optional extension to SyncTransport).
86
+ * When present, enables blob upload/download through the same transport.
87
+ */
88
+ export interface SyncTransportBlobs {
89
+ /**
90
+ * Initiate a blob upload.
91
+ * Returns presigned URL info or indicates blob already exists (dedup).
92
+ */
93
+ initiateUpload(args: {
94
+ hash: string;
95
+ size: number;
96
+ mimeType: string;
97
+ }): Promise<{
98
+ exists: boolean;
99
+ uploadUrl?: string;
100
+ uploadMethod?: 'PUT' | 'POST';
101
+ uploadHeaders?: Record<string, string>;
102
+ }>;
103
+
104
+ /**
105
+ * Complete a blob upload.
106
+ * Call this after uploading to the presigned URL.
107
+ */
108
+ completeUpload(hash: string): Promise<{ ok: boolean; error?: string }>;
109
+
110
+ /**
111
+ * Get a presigned download URL.
112
+ */
113
+ getDownloadUrl(hash: string): Promise<{
114
+ url: string;
115
+ expiresAt: string;
116
+ }>;
117
+ }
118
+
119
+ /**
120
+ * Transport interface for sync operations.
121
+ */
122
+ export interface SyncTransport {
123
+ /**
124
+ * Combined push+pull in a single round-trip.
125
+ */
126
+ sync(
127
+ request: SyncCombinedRequest,
128
+ options?: SyncTransportOptions
129
+ ): Promise<SyncCombinedResponse>;
130
+
131
+ /**
132
+ * Download an encoded bootstrap snapshot chunk.
133
+ */
134
+ fetchSnapshotChunk(
135
+ request: { chunkId: string },
136
+ options?: SyncTransportOptions
137
+ ): Promise<Uint8Array>;
138
+
139
+ /**
140
+ * Optional blob operations.
141
+ * When present, enables blob upload/download functionality.
142
+ */
143
+ blobs?: SyncTransportBlobs;
144
+ }
145
+
146
+ /**
147
+ * Transport error with additional context
148
+ */
149
+ export class SyncTransportError extends Error {
150
+ constructor(
151
+ message: string,
152
+ public readonly status?: number,
153
+ public readonly code?: string
154
+ ) {
155
+ super(message);
156
+ this.name = 'SyncTransportError';
157
+ }
158
+ }
@@ -0,0 +1,7 @@
1
+ export function randomId(): string {
2
+ const cryptoObj = globalThis.crypto;
3
+ if (cryptoObj && typeof cryptoObj.randomUUID === 'function') {
4
+ return cryptoObj.randomUUID();
5
+ }
6
+ return `${Date.now()}-${Math.random().toString(16).slice(2)}`;
7
+ }
@@ -0,0 +1,2 @@
1
+ export * from './id';
2
+ export * from './object';
@@ -0,0 +1,3 @@
1
+ export function isRecord(value: unknown): value is Record<string, unknown> {
2
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
3
+ }