@syncular/core 0.0.6-96 → 0.1.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/dist/blobs.d.ts +14 -7
- package/dist/blobs.d.ts.map +1 -1
- package/dist/blobs.js.map +1 -1
- package/dist/column-codecs.d.ts +3 -0
- package/dist/column-codecs.d.ts.map +1 -1
- package/dist/column-codecs.js +32 -4
- package/dist/column-codecs.js.map +1 -1
- package/dist/conflict.js.map +1 -1
- package/dist/database.js +1 -1
- package/dist/database.js.map +1 -1
- package/dist/error-responses.d.ts +393 -0
- package/dist/error-responses.d.ts.map +1 -0
- package/dist/error-responses.js +393 -0
- package/dist/error-responses.js.map +1 -0
- package/dist/http/api-client.d.ts +11 -0
- package/dist/http/api-client.d.ts.map +1 -0
- package/dist/http/api-client.js +30 -0
- package/dist/http/api-client.js.map +1 -0
- package/dist/http/blob.d.ts +6 -0
- package/dist/http/blob.d.ts.map +1 -0
- package/dist/http/blob.js +46 -0
- package/dist/http/blob.js.map +1 -0
- package/dist/http/generated/api.d.ts +5241 -0
- package/dist/http/generated/api.d.ts.map +1 -0
- package/dist/http/generated/api.js +6 -0
- package/dist/http/generated/api.js.map +1 -0
- package/dist/http/index.d.ts +18 -0
- package/dist/http/index.d.ts.map +1 -0
- package/dist/http/index.js +212 -0
- package/dist/http/index.js.map +1 -0
- package/dist/http/shared.d.ts +45 -0
- package/dist/http/shared.d.ts.map +1 -0
- package/dist/http/shared.js +122 -0
- package/dist/http/shared.js.map +1 -0
- package/dist/http/transport-client.d.ts +31 -0
- package/dist/http/transport-client.d.ts.map +1 -0
- package/dist/http/transport-client.js +193 -0
- package/dist/http/transport-client.js.map +1 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +19 -15
- package/dist/index.js.map +1 -1
- package/dist/kysely-column-codecs.d.ts +17 -1
- package/dist/kysely-column-codecs.d.ts.map +1 -1
- package/dist/kysely-column-codecs.js +56 -70
- package/dist/kysely-column-codecs.js.map +1 -1
- package/dist/logger.js +1 -1
- package/dist/logger.js.map +1 -1
- package/dist/schemas/blobs.d.ts +7 -1
- package/dist/schemas/blobs.d.ts.map +1 -1
- package/dist/schemas/blobs.js +1 -0
- package/dist/schemas/blobs.js.map +1 -1
- package/dist/schemas/common.d.ts +73 -1
- package/dist/schemas/common.d.ts.map +1 -1
- package/dist/schemas/common.js +38 -0
- package/dist/schemas/common.js.map +1 -1
- package/dist/schemas/index.js +3 -3
- package/dist/schemas/sync.d.ts +712 -38
- package/dist/schemas/sync.d.ts.map +1 -1
- package/dist/schemas/sync.js +209 -5
- package/dist/schemas/sync.js.map +1 -1
- package/dist/scopes/index.d.ts +5 -0
- package/dist/scopes/index.d.ts.map +1 -1
- package/dist/scopes/index.js +27 -0
- package/dist/scopes/index.js.map +1 -1
- package/dist/sentry.d.ts +31 -0
- package/dist/sentry.d.ts.map +1 -0
- package/dist/sentry.js +147 -0
- package/dist/sentry.js.map +1 -0
- package/dist/snapshot-chunks.d.ts +104 -20
- package/dist/snapshot-chunks.d.ts.map +1 -1
- package/dist/snapshot-chunks.js +665 -72
- package/dist/snapshot-chunks.js.map +1 -1
- package/dist/sync-packs.d.ts +33 -0
- package/dist/sync-packs.d.ts.map +1 -0
- package/dist/sync-packs.js +821 -0
- package/dist/sync-packs.js.map +1 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +3 -2
- package/dist/telemetry.js.map +1 -1
- package/dist/transforms.js.map +1 -1
- package/dist/types.d.ts +48 -7
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/dist/utils/bytes.d.ts +4 -0
- package/dist/utils/bytes.d.ts.map +1 -0
- package/dist/utils/bytes.js +66 -0
- package/dist/utils/bytes.js.map +1 -0
- package/dist/utils/compression.d.ts +11 -3
- package/dist/utils/compression.d.ts.map +1 -1
- package/dist/utils/compression.js +102 -50
- package/dist/utils/compression.js.map +1 -1
- package/dist/utils/crypto.d.ts +7 -1
- package/dist/utils/crypto.d.ts.map +1 -1
- package/dist/utils/crypto.js +89 -7
- package/dist/utils/crypto.js.map +1 -1
- package/dist/utils/id.d.ts.map +1 -1
- package/dist/utils/id.js +4 -1
- package/dist/utils/id.js.map +1 -1
- package/dist/utils/index.d.ts +5 -0
- package/dist/utils/index.d.ts.map +1 -1
- package/dist/utils/index.js +9 -4
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/internal-runtime.d.ts +16 -0
- package/dist/utils/internal-runtime.d.ts.map +1 -0
- package/dist/utils/internal-runtime.js +14 -0
- package/dist/utils/internal-runtime.js.map +1 -0
- package/dist/utils/object.js.map +1 -1
- package/dist/utils/realtime-change-index.d.ts +9 -0
- package/dist/utils/realtime-change-index.d.ts.map +1 -0
- package/dist/utils/realtime-change-index.js +36 -0
- package/dist/utils/realtime-change-index.js.map +1 -0
- package/dist/utils/realtime-connection-registry.d.ts +40 -0
- package/dist/utils/realtime-connection-registry.d.ts.map +1 -0
- package/dist/utils/realtime-connection-registry.js +237 -0
- package/dist/utils/realtime-connection-registry.js.map +1 -0
- package/dist/utils/table-registry.d.ts +10 -0
- package/dist/utils/table-registry.d.ts.map +1 -0
- package/dist/utils/table-registry.js +54 -0
- package/dist/utils/table-registry.js.map +1 -0
- package/dist/utils/url.d.ts +3 -0
- package/dist/utils/url.d.ts.map +1 -0
- package/dist/utils/url.js +20 -0
- package/dist/utils/url.js.map +1 -0
- package/package.json +52 -7
- package/src/blobs.ts +24 -7
- package/src/column-codecs.ts +36 -8
- package/src/error-responses.ts +423 -0
- package/src/http/api-client.ts +39 -0
- package/src/http/blob.ts +95 -0
- package/src/http/generated/api.ts +5241 -0
- package/src/http/index.ts +365 -0
- package/src/http/shared.ts +220 -0
- package/src/http/transport-client.ts +314 -0
- package/src/index.ts +4 -0
- package/src/kysely-column-codecs.ts +81 -86
- package/src/schemas/blobs.ts +10 -1
- package/src/schemas/common.ts +44 -0
- package/src/schemas/sync.ts +298 -7
- package/src/scopes/index.ts +42 -0
- package/src/sentry.ts +200 -0
- package/src/snapshot-chunks.ts +921 -69
- package/src/sync-packs.ts +1096 -0
- package/src/telemetry.ts +7 -2
- package/src/types.ts +63 -8
- package/src/utils/bytes.ts +75 -0
- package/src/utils/compression.ts +147 -51
- package/src/utils/crypto.ts +120 -7
- package/src/utils/id.ts +11 -1
- package/src/utils/index.ts +5 -0
- package/src/utils/internal-runtime.ts +39 -0
- package/src/utils/realtime-change-index.ts +44 -0
- package/src/utils/realtime-connection-registry.ts +286 -0
- package/src/utils/table-registry.ts +67 -0
- package/src/utils/url.ts +29 -0
- package/src/__tests__/compression.test.ts +0 -54
- package/src/__tests__/conflict.test.ts +0 -325
- package/src/__tests__/telemetry.test.ts +0 -170
- package/src/__tests__/utils.test.ts +0 -27
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
SyncAuthErrorContext,
|
|
3
|
+
SyncAuthLifecycle,
|
|
4
|
+
SyncCombinedRequest,
|
|
5
|
+
SyncCombinedResponse,
|
|
6
|
+
} from '@syncular/core';
|
|
7
|
+
import {
|
|
8
|
+
decodeBinarySyncPack,
|
|
9
|
+
isBinarySyncPackContentType,
|
|
10
|
+
} from '@syncular/core';
|
|
11
|
+
import type { SyncClient } from './api-client';
|
|
12
|
+
import {
|
|
13
|
+
type ApiResult,
|
|
14
|
+
type ClientOptions,
|
|
15
|
+
encodeSnapshotScopes,
|
|
16
|
+
type ResolveAuthRetry,
|
|
17
|
+
resolveRequestUrl,
|
|
18
|
+
SNAPSHOT_SCOPES_HEADER,
|
|
19
|
+
} from './shared';
|
|
20
|
+
|
|
21
|
+
export type { ClientOptions };
|
|
22
|
+
|
|
23
|
+
export const HTTP_TRANSPORT_SOURCE = Symbol.for('@syncular/core/http/source');
|
|
24
|
+
|
|
25
|
+
export type HttpTransportSource = SyncClient | ClientOptions;
|
|
26
|
+
|
|
27
|
+
function isApiClientLike(value: unknown): value is SyncClient {
|
|
28
|
+
return (
|
|
29
|
+
typeof value === 'object' &&
|
|
30
|
+
value !== null &&
|
|
31
|
+
typeof (value as SyncClient).GET === 'function' &&
|
|
32
|
+
typeof (value as SyncClient).POST === 'function'
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface TransportApiClient {
|
|
37
|
+
sync(
|
|
38
|
+
request: SyncCombinedRequest,
|
|
39
|
+
signal?: AbortSignal
|
|
40
|
+
): Promise<ApiResult<SyncCombinedResponse>>;
|
|
41
|
+
initiateUpload(
|
|
42
|
+
args: { hash: string; size: number; mimeType: string },
|
|
43
|
+
signal?: AbortSignal
|
|
44
|
+
): Promise<
|
|
45
|
+
ApiResult<{
|
|
46
|
+
exists: boolean;
|
|
47
|
+
uploadUrl?: string;
|
|
48
|
+
uploadMethod?: 'PUT' | 'POST';
|
|
49
|
+
uploadHeaders?: Record<string, string>;
|
|
50
|
+
}>
|
|
51
|
+
>;
|
|
52
|
+
completeUpload(
|
|
53
|
+
hash: string,
|
|
54
|
+
signal?: AbortSignal
|
|
55
|
+
): Promise<ApiResult<{ ok: boolean; error?: string }>>;
|
|
56
|
+
getDownloadUrl(
|
|
57
|
+
hash: string,
|
|
58
|
+
signal?: AbortSignal
|
|
59
|
+
): Promise<ApiResult<{ url: string; expiresAt: string }>>;
|
|
60
|
+
getSnapshotChunk(
|
|
61
|
+
chunkId: string,
|
|
62
|
+
scopeValues: Record<string, string | string[]> | undefined,
|
|
63
|
+
signal?: AbortSignal
|
|
64
|
+
): Promise<ApiResult<Blob>>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function isJsonContentType(contentType: string | null): boolean {
|
|
68
|
+
return contentType?.includes('application/json') === true;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function parseErrorBody(response: Response): Promise<unknown> {
|
|
72
|
+
if (!isJsonContentType(response.headers.get('content-type'))) {
|
|
73
|
+
return undefined;
|
|
74
|
+
}
|
|
75
|
+
try {
|
|
76
|
+
return (await response.json()) as unknown;
|
|
77
|
+
} catch {
|
|
78
|
+
return undefined;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function parseJsonBody<T>(response: Response): Promise<T | undefined> {
|
|
83
|
+
if (!isJsonContentType(response.headers.get('content-type'))) {
|
|
84
|
+
return undefined;
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
return (await response.json()) as T;
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function createAuthRetryResolver(
|
|
94
|
+
defaultAuthLifecycle: SyncAuthLifecycle | undefined
|
|
95
|
+
): ResolveAuthRetry {
|
|
96
|
+
let refreshInFlight: Promise<boolean> | null = null;
|
|
97
|
+
|
|
98
|
+
const runRefreshSingleFlight = async (
|
|
99
|
+
lifecycle: SyncAuthLifecycle,
|
|
100
|
+
context: SyncAuthErrorContext
|
|
101
|
+
): Promise<boolean> => {
|
|
102
|
+
if (!lifecycle.refreshToken) return false;
|
|
103
|
+
|
|
104
|
+
if (!refreshInFlight) {
|
|
105
|
+
refreshInFlight = Promise.resolve(lifecycle.refreshToken(context))
|
|
106
|
+
.then((result) => Boolean(result))
|
|
107
|
+
.finally(() => {
|
|
108
|
+
refreshInFlight = null;
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return refreshInFlight;
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
return async (context, options) => {
|
|
116
|
+
const lifecycle = options?.authLifecycle ?? defaultAuthLifecycle;
|
|
117
|
+
if (!lifecycle) return false;
|
|
118
|
+
|
|
119
|
+
await lifecycle.onAuthExpired?.(context);
|
|
120
|
+
|
|
121
|
+
const refreshResult = await runRefreshSingleFlight(lifecycle, context);
|
|
122
|
+
if (lifecycle.retryWithFreshToken) {
|
|
123
|
+
return Boolean(
|
|
124
|
+
await lifecycle.retryWithFreshToken({ ...context, refreshResult })
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
return refreshResult;
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function createRequestHeaders(
|
|
132
|
+
baseOptions: ClientOptions,
|
|
133
|
+
extraHeaders?: Record<string, string>
|
|
134
|
+
): Promise<Headers> {
|
|
135
|
+
return Promise.resolve(baseOptions.getHeaders?.()).then((dynamicHeaders) => {
|
|
136
|
+
const headers = new Headers(extraHeaders);
|
|
137
|
+
if (dynamicHeaders) {
|
|
138
|
+
for (const [key, value] of Object.entries(dynamicHeaders)) {
|
|
139
|
+
headers.set(key, value);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (!headers.has('x-syncular-transport-path')) {
|
|
143
|
+
headers.set(
|
|
144
|
+
'x-syncular-transport-path',
|
|
145
|
+
baseOptions.transportPath ?? 'direct'
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
return headers;
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function createFetchApiClient(baseOptions: ClientOptions): TransportApiClient {
|
|
153
|
+
const fetchImpl = baseOptions.fetch ?? globalThis.fetch;
|
|
154
|
+
|
|
155
|
+
const request = async <T>(args: {
|
|
156
|
+
method: 'GET' | 'POST';
|
|
157
|
+
path: string;
|
|
158
|
+
body?: unknown;
|
|
159
|
+
headers?: Record<string, string>;
|
|
160
|
+
signal?: AbortSignal;
|
|
161
|
+
parseAs?: 'json' | 'blob';
|
|
162
|
+
}): Promise<ApiResult<T>> => {
|
|
163
|
+
const headers = await createRequestHeaders(baseOptions, args.headers);
|
|
164
|
+
let body: BodyInit | undefined;
|
|
165
|
+
if (args.body !== undefined) {
|
|
166
|
+
headers.set('content-type', 'application/json');
|
|
167
|
+
body = JSON.stringify(args.body);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
const response = await fetchImpl(
|
|
171
|
+
resolveRequestUrl(baseOptions.baseUrl, args.path),
|
|
172
|
+
{
|
|
173
|
+
method: args.method,
|
|
174
|
+
headers,
|
|
175
|
+
body,
|
|
176
|
+
...(args.signal ? { signal: args.signal } : {}),
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
|
|
180
|
+
if (!response.ok) {
|
|
181
|
+
return {
|
|
182
|
+
response,
|
|
183
|
+
error: await parseErrorBody(response),
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (args.parseAs === 'blob') {
|
|
188
|
+
return {
|
|
189
|
+
response,
|
|
190
|
+
data: (await response.blob()) as T,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (isBinarySyncPackContentType(response.headers.get('content-type'))) {
|
|
195
|
+
return {
|
|
196
|
+
response,
|
|
197
|
+
data: decodeBinarySyncPack(
|
|
198
|
+
new Uint8Array(await response.arrayBuffer())
|
|
199
|
+
) as T,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return {
|
|
204
|
+
response,
|
|
205
|
+
data: await parseJsonBody<T>(response),
|
|
206
|
+
};
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
return {
|
|
210
|
+
sync: (requestBody, signal) =>
|
|
211
|
+
request({
|
|
212
|
+
method: 'POST',
|
|
213
|
+
path: '/sync',
|
|
214
|
+
body: requestBody,
|
|
215
|
+
signal,
|
|
216
|
+
}),
|
|
217
|
+
initiateUpload: (args, signal) =>
|
|
218
|
+
request({
|
|
219
|
+
method: 'POST',
|
|
220
|
+
path: '/sync/blobs/upload',
|
|
221
|
+
body: args,
|
|
222
|
+
signal,
|
|
223
|
+
}),
|
|
224
|
+
completeUpload: (hash, signal) =>
|
|
225
|
+
request({
|
|
226
|
+
method: 'POST',
|
|
227
|
+
path: `/sync/blobs/${encodeURIComponent(hash)}/complete`,
|
|
228
|
+
signal,
|
|
229
|
+
}),
|
|
230
|
+
getDownloadUrl: (hash, signal) =>
|
|
231
|
+
request({
|
|
232
|
+
method: 'GET',
|
|
233
|
+
path: `/sync/blobs/${encodeURIComponent(hash)}/url`,
|
|
234
|
+
signal,
|
|
235
|
+
}),
|
|
236
|
+
getSnapshotChunk: (chunkId, scopeValues, signal) =>
|
|
237
|
+
request({
|
|
238
|
+
method: 'GET',
|
|
239
|
+
path: `/sync/snapshot-chunks/${encodeURIComponent(chunkId)}`,
|
|
240
|
+
headers: (() => {
|
|
241
|
+
const encodedScopes = encodeSnapshotScopes(scopeValues);
|
|
242
|
+
return encodedScopes
|
|
243
|
+
? {
|
|
244
|
+
[SNAPSHOT_SCOPES_HEADER]: encodedScopes,
|
|
245
|
+
}
|
|
246
|
+
: undefined;
|
|
247
|
+
})(),
|
|
248
|
+
signal,
|
|
249
|
+
parseAs: 'blob',
|
|
250
|
+
}),
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function createTypedTransportClient(client: SyncClient): TransportApiClient {
|
|
255
|
+
return {
|
|
256
|
+
sync: (request, signal) =>
|
|
257
|
+
client.POST('/sync', {
|
|
258
|
+
body: request,
|
|
259
|
+
...(signal ? { signal } : {}),
|
|
260
|
+
}) as Promise<ApiResult<SyncCombinedResponse>>,
|
|
261
|
+
initiateUpload: (args, signal) =>
|
|
262
|
+
client.POST('/sync/blobs/upload', {
|
|
263
|
+
body: args,
|
|
264
|
+
...(signal ? { signal } : {}),
|
|
265
|
+
}) as Promise<
|
|
266
|
+
ApiResult<{
|
|
267
|
+
exists: boolean;
|
|
268
|
+
uploadUrl?: string;
|
|
269
|
+
uploadMethod?: 'PUT' | 'POST';
|
|
270
|
+
uploadHeaders?: Record<string, string>;
|
|
271
|
+
}>
|
|
272
|
+
>,
|
|
273
|
+
completeUpload: (hash, signal) =>
|
|
274
|
+
client.POST('/sync/blobs/{hash}/complete', {
|
|
275
|
+
params: { path: { hash } },
|
|
276
|
+
...(signal ? { signal } : {}),
|
|
277
|
+
}) as Promise<ApiResult<{ ok: boolean; error?: string }>>,
|
|
278
|
+
getDownloadUrl: (hash, signal) =>
|
|
279
|
+
client.GET('/sync/blobs/{hash}/url', {
|
|
280
|
+
params: { path: { hash } },
|
|
281
|
+
...(signal ? { signal } : {}),
|
|
282
|
+
}) as Promise<ApiResult<{ url: string; expiresAt: string }>>,
|
|
283
|
+
getSnapshotChunk: (chunkId, scopeValues, signal) =>
|
|
284
|
+
client.GET('/sync/snapshot-chunks/{chunkId}', {
|
|
285
|
+
params: { path: { chunkId } },
|
|
286
|
+
parseAs: 'blob',
|
|
287
|
+
headers: (() => {
|
|
288
|
+
const encodedScopes = encodeSnapshotScopes(scopeValues);
|
|
289
|
+
return encodedScopes
|
|
290
|
+
? {
|
|
291
|
+
[SNAPSHOT_SCOPES_HEADER]: encodedScopes,
|
|
292
|
+
}
|
|
293
|
+
: undefined;
|
|
294
|
+
})(),
|
|
295
|
+
...(signal ? { signal } : {}),
|
|
296
|
+
}) as Promise<ApiResult<Blob>>,
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function createTransportApiClient(
|
|
301
|
+
source: HttpTransportSource
|
|
302
|
+
): TransportApiClient {
|
|
303
|
+
return isApiClientLike(source)
|
|
304
|
+
? createTypedTransportClient(source)
|
|
305
|
+
: createFetchApiClient(source);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
export function createTransportAuthRetryResolver(
|
|
309
|
+
source: HttpTransportSource
|
|
310
|
+
): ResolveAuthRetry {
|
|
311
|
+
return createAuthRetryResolver(
|
|
312
|
+
isApiClientLike(source) ? undefined : source.authLifecycle
|
|
313
|
+
);
|
|
314
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -20,6 +20,8 @@ export * from './conflict';
|
|
|
20
20
|
export * from './database';
|
|
21
21
|
// Database dialect descriptors
|
|
22
22
|
export * from './dialect';
|
|
23
|
+
// Public error response envelopes
|
|
24
|
+
export * from './error-responses';
|
|
23
25
|
// Kysely plugin for applying column codecs in generic queries
|
|
24
26
|
export * from './kysely-column-codecs';
|
|
25
27
|
// Logging utilities
|
|
@@ -32,6 +34,8 @@ export * from './schemas';
|
|
|
32
34
|
export * from './scopes';
|
|
33
35
|
// Snapshot chunk encoding helpers
|
|
34
36
|
export * from './snapshot-chunks';
|
|
37
|
+
// Sync response pack encoding helpers
|
|
38
|
+
export * from './sync-packs';
|
|
35
39
|
// Telemetry abstraction
|
|
36
40
|
export * from './telemetry';
|
|
37
41
|
// Data transformation hooks
|
|
@@ -28,8 +28,8 @@ import {
|
|
|
28
28
|
applyCodecToDbValue,
|
|
29
29
|
type ColumnCodecDialect,
|
|
30
30
|
type ColumnCodecSource,
|
|
31
|
+
createTableColumnCodecsResolver,
|
|
31
32
|
type TableColumnCodecs,
|
|
32
|
-
toTableColumnCodecs,
|
|
33
33
|
} from './column-codecs';
|
|
34
34
|
|
|
35
35
|
interface ColumnCodecPluginOptions {
|
|
@@ -43,7 +43,7 @@ interface ColumnReference {
|
|
|
43
43
|
column: string;
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
interface QueryResultPlan {
|
|
46
|
+
export interface QueryResultPlan {
|
|
47
47
|
explicit: ColumnReference[];
|
|
48
48
|
selectAllTables: string[];
|
|
49
49
|
}
|
|
@@ -335,7 +335,9 @@ function planForDelete(node: DeleteQueryNode): QueryResultPlan | null {
|
|
|
335
335
|
return collected;
|
|
336
336
|
}
|
|
337
337
|
|
|
338
|
-
function
|
|
338
|
+
export function buildQueryResultPlan(
|
|
339
|
+
node: RootOperationNode
|
|
340
|
+
): QueryResultPlan | null {
|
|
339
341
|
if (node.kind === 'SelectQueryNode') return planForSelect(node);
|
|
340
342
|
if (node.kind === 'InsertQueryNode') return planForInsert(node);
|
|
341
343
|
if (node.kind === 'UpdateQueryNode') return planForUpdate(node);
|
|
@@ -343,35 +345,73 @@ function buildResultPlan(node: RootOperationNode): QueryResultPlan | null {
|
|
|
343
345
|
return null;
|
|
344
346
|
}
|
|
345
347
|
|
|
346
|
-
function
|
|
347
|
-
|
|
348
|
-
|
|
348
|
+
export function applyQueryResultPlan(args: {
|
|
349
|
+
row: UnknownRow;
|
|
350
|
+
plan: QueryResultPlan;
|
|
351
|
+
resolveTableCodecs: (
|
|
352
|
+
table: string,
|
|
353
|
+
row: Record<string, unknown>
|
|
354
|
+
) => TableColumnCodecs;
|
|
355
|
+
dialect: ColumnCodecDialect;
|
|
356
|
+
}): UnknownRow {
|
|
357
|
+
const { row, plan, resolveTableCodecs, dialect } = args;
|
|
358
|
+
const source = row as Record<string, unknown>;
|
|
359
|
+
const target: Record<string, unknown> = { ...source };
|
|
360
|
+
|
|
361
|
+
if (plan.selectAllTables.length > 0) {
|
|
362
|
+
const codecCandidates = new Map<string, AnyColumnCodec>();
|
|
363
|
+
const ambiguousColumns = new Set<string>();
|
|
364
|
+
|
|
365
|
+
for (const table of plan.selectAllTables) {
|
|
366
|
+
const tableCodecs = resolveTableCodecs(table, source);
|
|
367
|
+
for (const [column, codec] of Object.entries(tableCodecs)) {
|
|
368
|
+
if (!(column in source)) continue;
|
|
369
|
+
const existing = codecCandidates.get(column);
|
|
370
|
+
if (existing && existing !== codec) {
|
|
371
|
+
ambiguousColumns.add(column);
|
|
372
|
+
continue;
|
|
373
|
+
}
|
|
374
|
+
codecCandidates.set(column, codec);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
for (const [column, codec] of codecCandidates.entries()) {
|
|
379
|
+
if (ambiguousColumns.has(column)) continue;
|
|
380
|
+
target[column] = applyCodecFromDbValue(codec, target[column], dialect);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
for (const ref of plan.explicit) {
|
|
385
|
+
if (!(ref.outputKey in source)) continue;
|
|
386
|
+
const tableCodecs = resolveTableCodecs(ref.table, {
|
|
387
|
+
[ref.column]: source[ref.outputKey],
|
|
388
|
+
});
|
|
389
|
+
const codec = tableCodecs[ref.column];
|
|
390
|
+
if (!codec) continue;
|
|
391
|
+
target[ref.outputKey] = applyCodecFromDbValue(
|
|
392
|
+
codec,
|
|
393
|
+
target[ref.outputKey],
|
|
394
|
+
dialect
|
|
395
|
+
);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
return target;
|
|
349
399
|
}
|
|
350
400
|
|
|
351
401
|
class ColumnCodecsTransformer extends OperationNodeTransformer {
|
|
352
|
-
readonly #codecs: ColumnCodecSource;
|
|
353
402
|
readonly #dialect: ColumnCodecDialect;
|
|
354
|
-
readonly #
|
|
403
|
+
readonly #resolveTableCodecs: (
|
|
404
|
+
table: string,
|
|
405
|
+
row: Record<string, unknown>
|
|
406
|
+
) => TableColumnCodecs;
|
|
355
407
|
#currentUpdateTable: string | null = null;
|
|
356
408
|
|
|
357
409
|
constructor(options: ColumnCodecPluginOptions) {
|
|
358
410
|
super();
|
|
359
|
-
this.#codecs = options.codecs;
|
|
360
411
|
this.#dialect = options.dialect ?? 'sqlite';
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
resolveTableCodecs(
|
|
364
|
-
table: string,
|
|
365
|
-
columns: readonly string[]
|
|
366
|
-
): TableColumnCodecs {
|
|
367
|
-
const key = cacheKey(table, columns);
|
|
368
|
-
const cached = this.#tableCodecsCache.get(key);
|
|
369
|
-
if (cached) return cached;
|
|
370
|
-
const resolved = toTableColumnCodecs(table, this.#codecs, columns, {
|
|
412
|
+
this.#resolveTableCodecs = createTableColumnCodecsResolver(options.codecs, {
|
|
371
413
|
dialect: this.#dialect,
|
|
372
414
|
});
|
|
373
|
-
this.#tableCodecsCache.set(key, resolved);
|
|
374
|
-
return resolved;
|
|
375
415
|
}
|
|
376
416
|
|
|
377
417
|
protected override transformInsertQuery(
|
|
@@ -393,7 +433,10 @@ class ColumnCodecsTransformer extends OperationNodeTransformer {
|
|
|
393
433
|
.filter((columnName): columnName is string => Boolean(columnName));
|
|
394
434
|
if (columns.length === 0) return transformed;
|
|
395
435
|
|
|
396
|
-
const tableCodecs = this
|
|
436
|
+
const tableCodecs = this.#resolveTableCodecs(
|
|
437
|
+
tableName,
|
|
438
|
+
Object.fromEntries(columns.map((column) => [column, null]))
|
|
439
|
+
);
|
|
397
440
|
|
|
398
441
|
let didChange = false;
|
|
399
442
|
const nextValueRows = transformed.values.values.map((valueNode) => {
|
|
@@ -456,9 +499,9 @@ class ColumnCodecsTransformer extends OperationNodeTransformer {
|
|
|
456
499
|
const columnName = getColumnName(transformed.column);
|
|
457
500
|
if (!columnName) return transformed;
|
|
458
501
|
|
|
459
|
-
const tableCodecs = this
|
|
460
|
-
columnName,
|
|
461
|
-
|
|
502
|
+
const tableCodecs = this.#resolveTableCodecs(this.#currentUpdateTable, {
|
|
503
|
+
[columnName]: null,
|
|
504
|
+
});
|
|
462
505
|
const codec = tableCodecs[columnName];
|
|
463
506
|
if (!codec) return transformed;
|
|
464
507
|
|
|
@@ -485,12 +528,16 @@ export class ColumnCodecsPlugin implements KyselyPlugin {
|
|
|
485
528
|
readonly #dialect: ColumnCodecDialect;
|
|
486
529
|
readonly #transformer: ColumnCodecsTransformer;
|
|
487
530
|
readonly #resultPlans = new WeakMap<object, QueryResultPlan>();
|
|
488
|
-
readonly #
|
|
489
|
-
|
|
531
|
+
readonly #resolveTableCodecs: (
|
|
532
|
+
table: string,
|
|
533
|
+
row: Record<string, unknown>
|
|
534
|
+
) => TableColumnCodecs;
|
|
490
535
|
|
|
491
536
|
constructor(options: ColumnCodecPluginOptions) {
|
|
492
537
|
this.#dialect = options.dialect ?? 'sqlite';
|
|
493
|
-
this.#
|
|
538
|
+
this.#resolveTableCodecs = createTableColumnCodecsResolver(options.codecs, {
|
|
539
|
+
dialect: this.#dialect,
|
|
540
|
+
});
|
|
494
541
|
this.#transformer = new ColumnCodecsTransformer(options);
|
|
495
542
|
}
|
|
496
543
|
|
|
@@ -499,7 +546,7 @@ export class ColumnCodecsPlugin implements KyselyPlugin {
|
|
|
499
546
|
queryId,
|
|
500
547
|
}: PluginTransformQueryArgs): RootOperationNode {
|
|
501
548
|
const transformed = this.#transformer.transformNode(node);
|
|
502
|
-
const plan =
|
|
549
|
+
const plan = buildQueryResultPlan(transformed);
|
|
503
550
|
if (plan) {
|
|
504
551
|
this.#resultPlans.set(queryId, plan);
|
|
505
552
|
}
|
|
@@ -521,65 +568,13 @@ export class ColumnCodecsPlugin implements KyselyPlugin {
|
|
|
521
568
|
};
|
|
522
569
|
}
|
|
523
570
|
|
|
524
|
-
#
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
const cached = this.#resultCodecCache.get(key);
|
|
530
|
-
if (cached) return cached;
|
|
531
|
-
const resolved = toTableColumnCodecs(table, this.#codecs, columns, {
|
|
571
|
+
#transformRow(row: UnknownRow, plan: QueryResultPlan): UnknownRow {
|
|
572
|
+
return applyQueryResultPlan({
|
|
573
|
+
row,
|
|
574
|
+
plan,
|
|
575
|
+
resolveTableCodecs: this.#resolveTableCodecs,
|
|
532
576
|
dialect: this.#dialect,
|
|
533
577
|
});
|
|
534
|
-
this.#resultCodecCache.set(key, resolved);
|
|
535
|
-
return resolved;
|
|
536
|
-
}
|
|
537
|
-
|
|
538
|
-
#transformRow(row: UnknownRow, plan: QueryResultPlan): UnknownRow {
|
|
539
|
-
const source = row as Record<string, unknown>;
|
|
540
|
-
const target: Record<string, unknown> = { ...source };
|
|
541
|
-
const rowColumns = Object.keys(source);
|
|
542
|
-
|
|
543
|
-
if (plan.selectAllTables.length > 0) {
|
|
544
|
-
const codecCandidates = new Map<string, AnyColumnCodec>();
|
|
545
|
-
const ambiguousColumns = new Set<string>();
|
|
546
|
-
|
|
547
|
-
for (const table of plan.selectAllTables) {
|
|
548
|
-
const tableCodecs = this.#resolveTableCodecs(table, rowColumns);
|
|
549
|
-
for (const [column, codec] of Object.entries(tableCodecs)) {
|
|
550
|
-
if (!(column in source)) continue;
|
|
551
|
-
const existing = codecCandidates.get(column);
|
|
552
|
-
if (existing && existing !== codec) {
|
|
553
|
-
ambiguousColumns.add(column);
|
|
554
|
-
continue;
|
|
555
|
-
}
|
|
556
|
-
codecCandidates.set(column, codec);
|
|
557
|
-
}
|
|
558
|
-
}
|
|
559
|
-
|
|
560
|
-
for (const [column, codec] of codecCandidates.entries()) {
|
|
561
|
-
if (ambiguousColumns.has(column)) continue;
|
|
562
|
-
target[column] = applyCodecFromDbValue(
|
|
563
|
-
codec,
|
|
564
|
-
target[column],
|
|
565
|
-
this.#dialect
|
|
566
|
-
);
|
|
567
|
-
}
|
|
568
|
-
}
|
|
569
|
-
|
|
570
|
-
for (const ref of plan.explicit) {
|
|
571
|
-
if (!(ref.outputKey in source)) continue;
|
|
572
|
-
const tableCodecs = this.#resolveTableCodecs(ref.table, [ref.column]);
|
|
573
|
-
const codec = tableCodecs[ref.column];
|
|
574
|
-
if (!codec) continue;
|
|
575
|
-
target[ref.outputKey] = applyCodecFromDbValue(
|
|
576
|
-
codec,
|
|
577
|
-
target[ref.outputKey],
|
|
578
|
-
this.#dialect
|
|
579
|
-
);
|
|
580
|
-
}
|
|
581
|
-
|
|
582
|
-
return target;
|
|
583
578
|
}
|
|
584
579
|
}
|
|
585
580
|
|
package/src/schemas/blobs.ts
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { z } from 'zod';
|
|
8
|
+
import type { SyncularErrorCode } from '../error-responses';
|
|
8
9
|
|
|
9
10
|
// ============================================================================
|
|
10
11
|
// Blob Reference Schema
|
|
@@ -72,12 +73,20 @@ export const BlobUploadCompleteResponseSchema = z.object({
|
|
|
72
73
|
ok: z.boolean(),
|
|
73
74
|
metadata: BlobMetadataSchema.optional(),
|
|
74
75
|
error: z.string().optional(),
|
|
76
|
+
code: z.string().optional(),
|
|
75
77
|
});
|
|
76
78
|
|
|
77
|
-
|
|
79
|
+
type BlobUploadCompleteResponseBase = z.infer<
|
|
78
80
|
typeof BlobUploadCompleteResponseSchema
|
|
79
81
|
>;
|
|
80
82
|
|
|
83
|
+
export type BlobUploadCompleteResponse = Omit<
|
|
84
|
+
BlobUploadCompleteResponseBase,
|
|
85
|
+
'code'
|
|
86
|
+
> & {
|
|
87
|
+
code?: SyncularErrorCode;
|
|
88
|
+
};
|
|
89
|
+
|
|
81
90
|
// ============================================================================
|
|
82
91
|
// Download URL Request/Response Schemas
|
|
83
92
|
// ============================================================================
|
package/src/schemas/common.ts
CHANGED
|
@@ -8,13 +8,57 @@ import { z } from 'zod';
|
|
|
8
8
|
// Error Response Schemas
|
|
9
9
|
// ============================================================================
|
|
10
10
|
|
|
11
|
+
export const SyncularErrorCategorySchema = z.enum([
|
|
12
|
+
'auth-required',
|
|
13
|
+
'forbidden',
|
|
14
|
+
'conflict',
|
|
15
|
+
'scope-revoked',
|
|
16
|
+
'offline',
|
|
17
|
+
'invalid-request',
|
|
18
|
+
'not-found',
|
|
19
|
+
'schema-mismatch',
|
|
20
|
+
'integrity-rejected',
|
|
21
|
+
'rate-limited',
|
|
22
|
+
'limit-exceeded',
|
|
23
|
+
'transport',
|
|
24
|
+
'storage',
|
|
25
|
+
'blob',
|
|
26
|
+
'server',
|
|
27
|
+
'internal',
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
export const SyncularErrorRecommendedActionSchema = z.enum([
|
|
31
|
+
'refreshAuth',
|
|
32
|
+
'checkPermissions',
|
|
33
|
+
'fixRequest',
|
|
34
|
+
'resetClientId',
|
|
35
|
+
'splitBatch',
|
|
36
|
+
'reduceInput',
|
|
37
|
+
'retryLater',
|
|
38
|
+
'forceResync',
|
|
39
|
+
'regenerateClient',
|
|
40
|
+
'upgradeClient',
|
|
41
|
+
'inspectStorage',
|
|
42
|
+
'inspectServer',
|
|
43
|
+
'resolveConflict',
|
|
44
|
+
'recreateClient',
|
|
45
|
+
]);
|
|
46
|
+
|
|
11
47
|
export const ErrorResponseSchema = z.object({
|
|
12
48
|
error: z.string(),
|
|
13
49
|
message: z.string().optional(),
|
|
14
50
|
code: z.string().optional(),
|
|
51
|
+
category: SyncularErrorCategorySchema.optional(),
|
|
52
|
+
retryable: z.boolean().optional(),
|
|
53
|
+
recommendedAction: SyncularErrorRecommendedActionSchema.optional(),
|
|
54
|
+
details: z.record(z.string(), z.unknown()).optional(),
|
|
15
55
|
});
|
|
16
56
|
|
|
17
57
|
export type ErrorResponse = z.infer<typeof ErrorResponseSchema>;
|
|
58
|
+
export type SyncularErrorCategory = z.infer<typeof SyncularErrorCategorySchema>;
|
|
59
|
+
export type SyncularErrorRecommendedAction = z.infer<
|
|
60
|
+
typeof SyncularErrorRecommendedActionSchema
|
|
61
|
+
>;
|
|
18
62
|
|
|
19
63
|
// ============================================================================
|
|
20
64
|
// Pagination Schemas
|