@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.
- package/dist/blobs.d.ts +146 -0
- package/dist/blobs.d.ts.map +1 -0
- package/dist/blobs.js +47 -0
- package/dist/blobs.js.map +1 -0
- package/dist/conflict.d.ts +22 -0
- package/dist/conflict.d.ts.map +1 -0
- package/dist/conflict.js +81 -0
- package/dist/conflict.js.map +1 -0
- package/dist/index.d.ts +24 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +36 -0
- package/dist/index.js.map +1 -0
- package/dist/kysely-serialize.d.ts +22 -0
- package/dist/kysely-serialize.d.ts.map +1 -0
- package/dist/kysely-serialize.js +147 -0
- package/dist/kysely-serialize.js.map +1 -0
- package/dist/logger.d.ts +29 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +26 -0
- package/dist/logger.js.map +1 -0
- package/dist/proxy/index.d.ts +5 -0
- package/dist/proxy/index.d.ts.map +1 -0
- package/dist/proxy/index.js +5 -0
- package/dist/proxy/index.js.map +1 -0
- package/dist/proxy/types.d.ts +54 -0
- package/dist/proxy/types.d.ts.map +1 -0
- package/dist/proxy/types.js +7 -0
- package/dist/proxy/types.js.map +1 -0
- package/dist/schemas/blobs.d.ts +76 -0
- package/dist/schemas/blobs.d.ts.map +1 -0
- package/dist/schemas/blobs.js +63 -0
- package/dist/schemas/blobs.js.map +1 -0
- package/dist/schemas/common.d.ts +28 -0
- package/dist/schemas/common.d.ts.map +1 -0
- package/dist/schemas/common.js +26 -0
- package/dist/schemas/common.js.map +1 -0
- package/dist/schemas/index.d.ts +7 -0
- package/dist/schemas/index.d.ts.map +1 -0
- package/dist/schemas/index.js +7 -0
- package/dist/schemas/index.js.map +1 -0
- package/dist/schemas/sync.d.ts +391 -0
- package/dist/schemas/sync.d.ts.map +1 -0
- package/dist/schemas/sync.js +157 -0
- package/dist/schemas/sync.js.map +1 -0
- package/dist/scopes/index.d.ts +65 -0
- package/dist/scopes/index.d.ts.map +1 -0
- package/dist/scopes/index.js +67 -0
- package/dist/scopes/index.js.map +1 -0
- package/dist/snapshot-chunks.d.ts +26 -0
- package/dist/snapshot-chunks.d.ts.map +1 -0
- package/dist/snapshot-chunks.js +89 -0
- package/dist/snapshot-chunks.js.map +1 -0
- package/dist/telemetry.d.ts +114 -0
- package/dist/telemetry.d.ts.map +1 -0
- package/dist/telemetry.js +113 -0
- package/dist/telemetry.js.map +1 -0
- package/dist/transforms.d.ts +146 -0
- package/dist/transforms.d.ts.map +1 -0
- package/dist/transforms.js +155 -0
- package/dist/transforms.js.map +1 -0
- package/dist/types.d.ts +129 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +20 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/id.d.ts +2 -0
- package/dist/utils/id.d.ts.map +1 -0
- package/dist/utils/id.js +8 -0
- package/dist/utils/id.js.map +1 -0
- package/dist/utils/index.d.ts +3 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/index.js +3 -0
- package/dist/utils/index.js.map +1 -0
- package/dist/utils/object.d.ts +2 -0
- package/dist/utils/object.d.ts.map +1 -0
- package/dist/utils/object.js +4 -0
- package/dist/utils/object.js.map +1 -0
- package/package.json +57 -0
- package/src/__tests__/conflict.test.ts +325 -0
- package/src/__tests__/telemetry.test.ts +170 -0
- package/src/__tests__/utils.test.ts +27 -0
- package/src/blobs.ts +202 -0
- package/src/conflict.ts +92 -0
- package/src/index.ts +36 -0
- package/src/kysely-serialize.ts +214 -0
- package/src/logger.ts +38 -0
- package/src/proxy/index.ts +10 -0
- package/src/proxy/types.ts +57 -0
- package/src/schemas/blobs.ts +101 -0
- package/src/schemas/common.ts +45 -0
- package/src/schemas/index.ts +7 -0
- package/src/schemas/sync.ts +226 -0
- package/src/scopes/index.ts +122 -0
- package/src/snapshot-chunks.ts +112 -0
- package/src/telemetry.ts +238 -0
- package/src/transforms.ts +256 -0
- package/src/types.ts +158 -0
- package/src/utils/id.ts +7 -0
- package/src/utils/index.ts +2 -0
- package/src/utils/object.ts +3 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/core - Blob Zod schemas
|
|
3
|
+
*
|
|
4
|
+
* Runtime validation schemas for blob types.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { z } from 'zod';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// Blob Reference Schema
|
|
11
|
+
// ============================================================================
|
|
12
|
+
|
|
13
|
+
export const BlobRefSchema = z.object({
|
|
14
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/i, 'Invalid blob hash format'),
|
|
15
|
+
size: z.number().int().min(0),
|
|
16
|
+
mimeType: z.string().min(1),
|
|
17
|
+
encrypted: z.boolean().optional(),
|
|
18
|
+
keyId: z.string().optional(),
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
export type BlobRef = z.infer<typeof BlobRefSchema>;
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Blob Metadata Schema
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
export const BlobMetadataSchema = z.object({
|
|
28
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/i),
|
|
29
|
+
size: z.number().int().min(0),
|
|
30
|
+
mimeType: z.string().min(1),
|
|
31
|
+
createdAt: z.string(),
|
|
32
|
+
expiresAt: z.string().optional(),
|
|
33
|
+
uploadComplete: z.boolean(),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
export type BlobMetadata = z.infer<typeof BlobMetadataSchema>;
|
|
37
|
+
|
|
38
|
+
// ============================================================================
|
|
39
|
+
// Upload Request/Response Schemas
|
|
40
|
+
// ============================================================================
|
|
41
|
+
|
|
42
|
+
export const BlobUploadInitRequestSchema = z.object({
|
|
43
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/i, 'Invalid blob hash format'),
|
|
44
|
+
size: z.number().int().min(0),
|
|
45
|
+
mimeType: z.string().min(1),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
export type BlobUploadInitRequest = z.infer<typeof BlobUploadInitRequestSchema>;
|
|
49
|
+
|
|
50
|
+
export const BlobUploadInitResponseSchema = z.object({
|
|
51
|
+
exists: z.boolean(),
|
|
52
|
+
uploadId: z.string().optional(),
|
|
53
|
+
uploadUrl: z.string().url().optional(),
|
|
54
|
+
uploadMethod: z.enum(['PUT', 'POST']).optional(),
|
|
55
|
+
uploadHeaders: z.record(z.string(), z.string()).optional(),
|
|
56
|
+
chunkSize: z.number().int().optional(),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
export type BlobUploadInitResponse = z.infer<
|
|
60
|
+
typeof BlobUploadInitResponseSchema
|
|
61
|
+
>;
|
|
62
|
+
|
|
63
|
+
export const BlobUploadCompleteRequestSchema = z.object({
|
|
64
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/i),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
export type BlobUploadCompleteRequest = z.infer<
|
|
68
|
+
typeof BlobUploadCompleteRequestSchema
|
|
69
|
+
>;
|
|
70
|
+
|
|
71
|
+
export const BlobUploadCompleteResponseSchema = z.object({
|
|
72
|
+
ok: z.boolean(),
|
|
73
|
+
metadata: BlobMetadataSchema.optional(),
|
|
74
|
+
error: z.string().optional(),
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
export type BlobUploadCompleteResponse = z.infer<
|
|
78
|
+
typeof BlobUploadCompleteResponseSchema
|
|
79
|
+
>;
|
|
80
|
+
|
|
81
|
+
// ============================================================================
|
|
82
|
+
// Download URL Request/Response Schemas
|
|
83
|
+
// ============================================================================
|
|
84
|
+
|
|
85
|
+
export const BlobDownloadUrlRequestSchema = z.object({
|
|
86
|
+
hash: z.string().regex(/^sha256:[0-9a-f]{64}$/i),
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
export type BlobDownloadUrlRequest = z.infer<
|
|
90
|
+
typeof BlobDownloadUrlRequestSchema
|
|
91
|
+
>;
|
|
92
|
+
|
|
93
|
+
export const BlobDownloadUrlResponseSchema = z.object({
|
|
94
|
+
url: z.string().url(),
|
|
95
|
+
expiresAt: z.string(),
|
|
96
|
+
metadata: BlobMetadataSchema,
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
export type BlobDownloadUrlResponse = z.infer<
|
|
100
|
+
typeof BlobDownloadUrlResponseSchema
|
|
101
|
+
>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/core - Common Zod schemas
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { z } from 'zod';
|
|
6
|
+
|
|
7
|
+
// ============================================================================
|
|
8
|
+
// Error Response Schemas
|
|
9
|
+
// ============================================================================
|
|
10
|
+
|
|
11
|
+
export const ErrorResponseSchema = z.object({
|
|
12
|
+
error: z.string(),
|
|
13
|
+
message: z.string().optional(),
|
|
14
|
+
code: z.string().optional(),
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
|
18
|
+
|
|
19
|
+
// ============================================================================
|
|
20
|
+
// Pagination Schemas
|
|
21
|
+
// ============================================================================
|
|
22
|
+
|
|
23
|
+
export const PaginationQuerySchema = z.object({
|
|
24
|
+
limit: z.coerce.number().int().min(1).max(100).default(50),
|
|
25
|
+
offset: z.coerce.number().int().min(0).default(0),
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
export type PaginationQuery = z.infer<typeof PaginationQuerySchema>;
|
|
29
|
+
|
|
30
|
+
export const PaginatedResponseSchema = <T extends z.ZodTypeAny>(
|
|
31
|
+
itemSchema: T
|
|
32
|
+
) =>
|
|
33
|
+
z.object({
|
|
34
|
+
items: z.array(itemSchema),
|
|
35
|
+
total: z.number().int(),
|
|
36
|
+
offset: z.number().int(),
|
|
37
|
+
limit: z.number().int(),
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
export type PaginatedResponse<T> = {
|
|
41
|
+
items: T[];
|
|
42
|
+
total: number;
|
|
43
|
+
offset: number;
|
|
44
|
+
limit: number;
|
|
45
|
+
};
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/core - Sync protocol Zod schemas
|
|
3
|
+
*
|
|
4
|
+
* These schemas define the sync protocol types and can be used for:
|
|
5
|
+
* - Runtime validation
|
|
6
|
+
* - OpenAPI spec generation
|
|
7
|
+
* - Type inference
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import {
|
|
12
|
+
SYNC_SNAPSHOT_CHUNK_COMPRESSION,
|
|
13
|
+
SYNC_SNAPSHOT_CHUNK_ENCODING,
|
|
14
|
+
} from '../snapshot-chunks';
|
|
15
|
+
|
|
16
|
+
// ============================================================================
|
|
17
|
+
// Operation Types
|
|
18
|
+
// ============================================================================
|
|
19
|
+
|
|
20
|
+
export const SyncOpSchema = z.enum(['upsert', 'delete']);
|
|
21
|
+
export type SyncOp = z.infer<typeof SyncOpSchema>;
|
|
22
|
+
|
|
23
|
+
// ============================================================================
|
|
24
|
+
// Scope Schemas
|
|
25
|
+
// ============================================================================
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Stored scopes on a change (single values only)
|
|
29
|
+
*/
|
|
30
|
+
const StoredScopesSchema = z.record(z.string(), z.string());
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Scope values in a subscription request (can be arrays)
|
|
34
|
+
*/
|
|
35
|
+
export const ScopeValuesSchema = z.record(
|
|
36
|
+
z.string(),
|
|
37
|
+
z.union([z.string(), z.array(z.string())])
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// Sync Operation Schema
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
export const SyncOperationSchema = z.object({
|
|
45
|
+
table: z.string(),
|
|
46
|
+
row_id: z.string(),
|
|
47
|
+
op: SyncOpSchema,
|
|
48
|
+
payload: z.record(z.string(), z.unknown()).nullable(),
|
|
49
|
+
base_version: z.number().int().nullable().optional(),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
export type SyncOperation = z.infer<typeof SyncOperationSchema>;
|
|
53
|
+
|
|
54
|
+
// ============================================================================
|
|
55
|
+
// Push Request/Response Schemas
|
|
56
|
+
// ============================================================================
|
|
57
|
+
|
|
58
|
+
export const SyncPushRequestSchema = z.object({
|
|
59
|
+
clientId: z.string().min(1),
|
|
60
|
+
clientCommitId: z.string().min(1),
|
|
61
|
+
operations: z.array(SyncOperationSchema).min(1),
|
|
62
|
+
schemaVersion: z.number().int().min(1),
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
export type SyncPushRequest = z.infer<typeof SyncPushRequestSchema>;
|
|
66
|
+
|
|
67
|
+
const SyncOperationResultAppliedSchema = z.object({
|
|
68
|
+
opIndex: z.number().int(),
|
|
69
|
+
status: z.literal('applied'),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
const SyncOperationResultConflictSchema = z.object({
|
|
73
|
+
opIndex: z.number().int(),
|
|
74
|
+
status: z.literal('conflict'),
|
|
75
|
+
message: z.string(),
|
|
76
|
+
server_version: z.number().int(),
|
|
77
|
+
server_row: z.unknown(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const SyncOperationResultErrorSchema = z.object({
|
|
81
|
+
opIndex: z.number().int(),
|
|
82
|
+
status: z.literal('error'),
|
|
83
|
+
error: z.string(),
|
|
84
|
+
code: z.string().optional(),
|
|
85
|
+
retriable: z.boolean().optional(),
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
export const SyncOperationResultSchema = z.union([
|
|
89
|
+
SyncOperationResultAppliedSchema,
|
|
90
|
+
SyncOperationResultConflictSchema,
|
|
91
|
+
SyncOperationResultErrorSchema,
|
|
92
|
+
]);
|
|
93
|
+
|
|
94
|
+
export type SyncOperationResult = z.infer<typeof SyncOperationResultSchema>;
|
|
95
|
+
|
|
96
|
+
export const SyncPushResponseSchema = z.object({
|
|
97
|
+
ok: z.literal(true),
|
|
98
|
+
status: z.enum(['applied', 'cached', 'rejected']),
|
|
99
|
+
commitSeq: z.number().int().optional(),
|
|
100
|
+
results: z.array(SyncOperationResultSchema),
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
export type SyncPushResponse = z.infer<typeof SyncPushResponseSchema>;
|
|
104
|
+
|
|
105
|
+
// ============================================================================
|
|
106
|
+
// Bootstrap State Schema
|
|
107
|
+
// ============================================================================
|
|
108
|
+
|
|
109
|
+
export const SyncBootstrapStateSchema = z.object({
|
|
110
|
+
asOfCommitSeq: z.number().int(),
|
|
111
|
+
tables: z.array(z.string()),
|
|
112
|
+
tableIndex: z.number().int(),
|
|
113
|
+
rowCursor: z.string().nullable(),
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
export type SyncBootstrapState = z.infer<typeof SyncBootstrapStateSchema>;
|
|
117
|
+
|
|
118
|
+
// ============================================================================
|
|
119
|
+
// Pull Request/Response Schemas
|
|
120
|
+
// ============================================================================
|
|
121
|
+
|
|
122
|
+
export const SyncSubscriptionRequestSchema = z.object({
|
|
123
|
+
id: z.string().min(1),
|
|
124
|
+
shape: z.string().min(1),
|
|
125
|
+
scopes: ScopeValuesSchema,
|
|
126
|
+
params: z.record(z.string(), z.unknown()).optional(),
|
|
127
|
+
cursor: z.number().int(),
|
|
128
|
+
bootstrapState: SyncBootstrapStateSchema.nullable().optional(),
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
export type SyncSubscriptionRequest = z.infer<
|
|
132
|
+
typeof SyncSubscriptionRequestSchema
|
|
133
|
+
>;
|
|
134
|
+
|
|
135
|
+
export const SyncPullRequestSchema = z.object({
|
|
136
|
+
clientId: z.string().min(1),
|
|
137
|
+
limitCommits: z.number().int().min(1),
|
|
138
|
+
limitSnapshotRows: z.number().int().min(1).optional(),
|
|
139
|
+
maxSnapshotPages: z.number().int().min(1).optional(),
|
|
140
|
+
dedupeRows: z.boolean().optional(),
|
|
141
|
+
subscriptions: z.array(SyncSubscriptionRequestSchema),
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
export type SyncPullRequest = z.infer<typeof SyncPullRequestSchema>;
|
|
145
|
+
|
|
146
|
+
export const SyncChangeSchema = z.object({
|
|
147
|
+
table: z.string(),
|
|
148
|
+
row_id: z.string(),
|
|
149
|
+
op: SyncOpSchema,
|
|
150
|
+
row_json: z.unknown().nullable(),
|
|
151
|
+
row_version: z.number().int().nullable(),
|
|
152
|
+
scopes: StoredScopesSchema,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
export type SyncChange = z.infer<typeof SyncChangeSchema>;
|
|
156
|
+
|
|
157
|
+
export const SyncCommitSchema = z.object({
|
|
158
|
+
commitSeq: z.number().int(),
|
|
159
|
+
createdAt: z.string(),
|
|
160
|
+
actorId: z.string(),
|
|
161
|
+
changes: z.array(SyncChangeSchema),
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
export type SyncCommit = z.infer<typeof SyncCommitSchema>;
|
|
165
|
+
|
|
166
|
+
export const SyncSnapshotChunkRefSchema = z.object({
|
|
167
|
+
id: z.string(),
|
|
168
|
+
byteLength: z.number().int(),
|
|
169
|
+
sha256: z.string(),
|
|
170
|
+
encoding: z.literal(SYNC_SNAPSHOT_CHUNK_ENCODING),
|
|
171
|
+
compression: z.literal(SYNC_SNAPSHOT_CHUNK_COMPRESSION),
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export type SyncSnapshotChunkRef = z.infer<typeof SyncSnapshotChunkRefSchema>;
|
|
175
|
+
|
|
176
|
+
export const SyncSnapshotSchema = z.object({
|
|
177
|
+
table: z.string(),
|
|
178
|
+
rows: z.array(z.unknown()),
|
|
179
|
+
chunks: z.array(SyncSnapshotChunkRefSchema).optional(),
|
|
180
|
+
isFirstPage: z.boolean(),
|
|
181
|
+
isLastPage: z.boolean(),
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
export type SyncSnapshot = z.infer<typeof SyncSnapshotSchema>;
|
|
185
|
+
|
|
186
|
+
export const SyncPullSubscriptionResponseSchema = z.object({
|
|
187
|
+
id: z.string(),
|
|
188
|
+
status: z.enum(['active', 'revoked']),
|
|
189
|
+
scopes: ScopeValuesSchema,
|
|
190
|
+
bootstrap: z.boolean(),
|
|
191
|
+
bootstrapState: SyncBootstrapStateSchema.nullable().optional(),
|
|
192
|
+
nextCursor: z.number().int(),
|
|
193
|
+
commits: z.array(SyncCommitSchema),
|
|
194
|
+
snapshots: z.array(SyncSnapshotSchema).optional(),
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
export type SyncPullSubscriptionResponse = z.infer<
|
|
198
|
+
typeof SyncPullSubscriptionResponseSchema
|
|
199
|
+
>;
|
|
200
|
+
|
|
201
|
+
export const SyncPullResponseSchema = z.object({
|
|
202
|
+
ok: z.literal(true),
|
|
203
|
+
subscriptions: z.array(SyncPullSubscriptionResponseSchema),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
export type SyncPullResponse = z.infer<typeof SyncPullResponseSchema>;
|
|
207
|
+
|
|
208
|
+
// ============================================================================
|
|
209
|
+
// Combined Sync Request/Response Schemas
|
|
210
|
+
// ============================================================================
|
|
211
|
+
|
|
212
|
+
export const SyncCombinedRequestSchema = z.object({
|
|
213
|
+
clientId: z.string().min(1),
|
|
214
|
+
push: SyncPushRequestSchema.omit({ clientId: true }).optional(),
|
|
215
|
+
pull: SyncPullRequestSchema.omit({ clientId: true }).optional(),
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
export type SyncCombinedRequest = z.infer<typeof SyncCombinedRequestSchema>;
|
|
219
|
+
|
|
220
|
+
export const SyncCombinedResponseSchema = z.object({
|
|
221
|
+
ok: z.literal(true),
|
|
222
|
+
push: SyncPushResponseSchema.optional(),
|
|
223
|
+
pull: SyncPullResponseSchema.optional(),
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
export type SyncCombinedResponse = z.infer<typeof SyncCombinedResponseSchema>;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/core - Scope types, patterns, and utilities
|
|
3
|
+
*
|
|
4
|
+
* Scope patterns define how data is partitioned for sync.
|
|
5
|
+
* Scopes are stored as JSONB on changes for flexible filtering.
|
|
6
|
+
* Patterns use `{placeholder}` syntax to extract or inject values.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ── Types ────────────────────────────────────────────────────────────
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Scope pattern string, e.g., 'user:{user_id}', 'project:{project_id}'
|
|
13
|
+
*/
|
|
14
|
+
export type ScopePattern = string;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Scope values - the actual values for scope variables.
|
|
18
|
+
* Values can be single strings or arrays (for multi-value subscriptions).
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* { user_id: 'U1' }
|
|
22
|
+
* { project_id: ['P1', 'P2'] }
|
|
23
|
+
* { year: '2025', month: '03' }
|
|
24
|
+
*/
|
|
25
|
+
export type ScopeValues = Record<string, string | string[]>;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Stored scopes on a change - always single values (not arrays).
|
|
29
|
+
* This is what gets stored in the JSONB column.
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* { user_id: 'U1', project_id: 'P1' }
|
|
33
|
+
*/
|
|
34
|
+
export type StoredScopes = Record<string, string>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Simplified scope definition.
|
|
38
|
+
* Can be a simple pattern string or an object with explicit column mapping.
|
|
39
|
+
*
|
|
40
|
+
* @example
|
|
41
|
+
* ```typescript
|
|
42
|
+
* // Simple: pattern column is auto-derived
|
|
43
|
+
* scopes: ['user:{user_id}', 'org:{org_id}']
|
|
44
|
+
*
|
|
45
|
+
* // Explicit: when column differs from pattern variable
|
|
46
|
+
* scopes: [
|
|
47
|
+
* { pattern: 'user:{user_id}', column: 'owner_id' }
|
|
48
|
+
* ]
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
export type ScopeDefinition = string | { pattern: string; column: string };
|
|
52
|
+
|
|
53
|
+
// ── Pattern parsing (internal helpers) ───────────────────────────────
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Extract the placeholder name from a pattern.
|
|
57
|
+
* Returns null if the pattern doesn't contain a valid placeholder.
|
|
58
|
+
*/
|
|
59
|
+
function extractPlaceholder(pattern: string): {
|
|
60
|
+
prefix: string;
|
|
61
|
+
placeholder: string;
|
|
62
|
+
suffix: string;
|
|
63
|
+
} | null {
|
|
64
|
+
const match = pattern.match(/^(.*?)\{(\w+)\}(.*)$/);
|
|
65
|
+
if (!match) return null;
|
|
66
|
+
|
|
67
|
+
return {
|
|
68
|
+
prefix: match[1]!,
|
|
69
|
+
placeholder: match[2]!,
|
|
70
|
+
suffix: match[3]!,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Extract the placeholder name from a pattern.
|
|
76
|
+
*/
|
|
77
|
+
function getPlaceholderName(pattern: string): string | null {
|
|
78
|
+
const parsed = extractPlaceholder(pattern);
|
|
79
|
+
return parsed?.placeholder ?? null;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Normalize scope definitions to a pattern-to-column map.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* normalizeScopes(['user:{user_id}'])
|
|
87
|
+
* // → { 'user:{user_id}': 'user_id' }
|
|
88
|
+
*/
|
|
89
|
+
export function normalizeScopes(
|
|
90
|
+
scopes: ScopeDefinition[]
|
|
91
|
+
): Record<string, string> {
|
|
92
|
+
const result: Record<string, string> = {};
|
|
93
|
+
for (const scope of scopes) {
|
|
94
|
+
if (typeof scope === 'string') {
|
|
95
|
+
const placeholder = getPlaceholderName(scope);
|
|
96
|
+
if (!placeholder) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`Scope pattern "${scope}" must contain a placeholder like {column_name}`
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
result[scope] = placeholder;
|
|
102
|
+
} else {
|
|
103
|
+
result[scope.pattern] = scope.column;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return result;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// ── Value operations (public) ────────────────────────────────────────
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Extract variable names from a scope pattern.
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* extractScopeVars('project:{project_id}') // ['project_id']
|
|
116
|
+
* extractScopeVars('event_date:{year}:{month}') // ['year', 'month']
|
|
117
|
+
*/
|
|
118
|
+
export function extractScopeVars(pattern: ScopePattern): string[] {
|
|
119
|
+
const matches = pattern.match(/\{([^}]+)\}/g);
|
|
120
|
+
if (!matches) return [];
|
|
121
|
+
return matches.map((m) => m.slice(1, -1));
|
|
122
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @syncular/core - Snapshot chunk encoding helpers
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
export const SYNC_SNAPSHOT_CHUNK_ENCODING = 'json-row-frame-v1';
|
|
6
|
+
export type SyncSnapshotChunkEncoding = typeof SYNC_SNAPSHOT_CHUNK_ENCODING;
|
|
7
|
+
|
|
8
|
+
export const SYNC_SNAPSHOT_CHUNK_COMPRESSION = 'gzip';
|
|
9
|
+
export type SyncSnapshotChunkCompression =
|
|
10
|
+
typeof SYNC_SNAPSHOT_CHUNK_COMPRESSION;
|
|
11
|
+
|
|
12
|
+
const SNAPSHOT_ROW_FRAME_MAGIC = new Uint8Array([0x53, 0x52, 0x46, 0x31]); // "SRF1"
|
|
13
|
+
const FRAME_LENGTH_BYTES = 4;
|
|
14
|
+
const MAX_FRAME_BYTE_LENGTH = 0xffff_ffff;
|
|
15
|
+
|
|
16
|
+
function normalizeRowJson(row: unknown): string {
|
|
17
|
+
const serialized = JSON.stringify(row);
|
|
18
|
+
return serialized === undefined ? 'null' : serialized;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Encode rows as framed JSON bytes without the format header.
|
|
23
|
+
*/
|
|
24
|
+
export function encodeSnapshotRowFrames(rows: readonly unknown[]): Uint8Array {
|
|
25
|
+
const encoder = new TextEncoder();
|
|
26
|
+
const payloads: Uint8Array[] = [];
|
|
27
|
+
let totalByteLength = 0;
|
|
28
|
+
|
|
29
|
+
for (const row of rows) {
|
|
30
|
+
const payload = encoder.encode(normalizeRowJson(row));
|
|
31
|
+
if (payload.length > MAX_FRAME_BYTE_LENGTH) {
|
|
32
|
+
throw new Error(
|
|
33
|
+
`Snapshot row payload exceeds ${MAX_FRAME_BYTE_LENGTH} bytes`
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
payloads.push(payload);
|
|
37
|
+
totalByteLength += FRAME_LENGTH_BYTES + payload.length;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const encoded = new Uint8Array(totalByteLength);
|
|
41
|
+
const view = new DataView(encoded.buffer, encoded.byteOffset, encoded.length);
|
|
42
|
+
let offset = 0;
|
|
43
|
+
for (const payload of payloads) {
|
|
44
|
+
view.setUint32(offset, payload.length, false);
|
|
45
|
+
offset += FRAME_LENGTH_BYTES;
|
|
46
|
+
encoded.set(payload, offset);
|
|
47
|
+
offset += payload.length;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
return encoded;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Encode rows as framed JSON bytes with a format header.
|
|
55
|
+
*
|
|
56
|
+
* Format:
|
|
57
|
+
* - 4-byte magic header ("SRF1")
|
|
58
|
+
* - repeated frames of:
|
|
59
|
+
* - 4-byte big-endian payload byte length
|
|
60
|
+
* - UTF-8 JSON payload
|
|
61
|
+
*/
|
|
62
|
+
export function encodeSnapshotRows(rows: readonly unknown[]): Uint8Array {
|
|
63
|
+
const framedRows = encodeSnapshotRowFrames(rows);
|
|
64
|
+
const totalByteLength = SNAPSHOT_ROW_FRAME_MAGIC.length + framedRows.length;
|
|
65
|
+
|
|
66
|
+
const encoded = new Uint8Array(totalByteLength);
|
|
67
|
+
encoded.set(SNAPSHOT_ROW_FRAME_MAGIC, 0);
|
|
68
|
+
encoded.set(framedRows, SNAPSHOT_ROW_FRAME_MAGIC.length);
|
|
69
|
+
|
|
70
|
+
return encoded;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Decode framed JSON bytes into rows.
|
|
75
|
+
*/
|
|
76
|
+
export function decodeSnapshotRows(bytes: Uint8Array): unknown[] {
|
|
77
|
+
if (bytes.length < SNAPSHOT_ROW_FRAME_MAGIC.length) {
|
|
78
|
+
throw new Error('Snapshot chunk payload is too small');
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
for (let index = 0; index < SNAPSHOT_ROW_FRAME_MAGIC.length; index += 1) {
|
|
82
|
+
const expected = SNAPSHOT_ROW_FRAME_MAGIC[index];
|
|
83
|
+
const actual = bytes[index];
|
|
84
|
+
if (actual !== expected) {
|
|
85
|
+
throw new Error('Unexpected snapshot chunk format');
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const rows: unknown[] = [];
|
|
90
|
+
const decoder = new TextDecoder();
|
|
91
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);
|
|
92
|
+
let offset = SNAPSHOT_ROW_FRAME_MAGIC.length;
|
|
93
|
+
|
|
94
|
+
while (offset < bytes.length) {
|
|
95
|
+
if (offset + FRAME_LENGTH_BYTES > bytes.length) {
|
|
96
|
+
throw new Error('Snapshot chunk payload ended mid-frame header');
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const payloadLength = view.getUint32(offset, false);
|
|
100
|
+
offset += FRAME_LENGTH_BYTES;
|
|
101
|
+
|
|
102
|
+
if (offset + payloadLength > bytes.length) {
|
|
103
|
+
throw new Error('Snapshot chunk payload ended mid-frame body');
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const payload = bytes.subarray(offset, offset + payloadLength);
|
|
107
|
+
offset += payloadLength;
|
|
108
|
+
rows.push(JSON.parse(decoder.decode(payload)));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return rows;
|
|
112
|
+
}
|