@syncular/core 0.0.6-96 → 0.1.1
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.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 +4 -0
- 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.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/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 +5 -0
- 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 +51 -6
- 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
package/src/telemetry.ts
CHANGED
|
@@ -113,13 +113,18 @@ const noopMetrics: SyncMetrics = {
|
|
|
113
113
|
distribution() {},
|
|
114
114
|
};
|
|
115
115
|
|
|
116
|
+
type DeferredRuntimeGlobals = typeof globalThis & {
|
|
117
|
+
setImmediate?: (callback: () => void) => void;
|
|
118
|
+
};
|
|
119
|
+
|
|
116
120
|
function createConsoleLogger(): (event: SyncTelemetryEvent) => void {
|
|
121
|
+
const runtimeGlobals = globalThis as DeferredRuntimeGlobals;
|
|
117
122
|
const isNode =
|
|
118
123
|
typeof globalThis !== 'undefined' &&
|
|
119
|
-
typeof
|
|
124
|
+
typeof runtimeGlobals.setImmediate === 'function';
|
|
120
125
|
|
|
121
126
|
const defer = isNode
|
|
122
|
-
? (fn: () => void) =>
|
|
127
|
+
? (fn: () => void) => runtimeGlobals.setImmediate?.(fn)
|
|
123
128
|
: (fn: () => void) => setTimeout(fn, 0);
|
|
124
129
|
|
|
125
130
|
return (event: SyncTelemetryEvent) => {
|
package/src/types.ts
CHANGED
|
@@ -61,6 +61,17 @@ export interface MergeResultConflict {
|
|
|
61
61
|
*/
|
|
62
62
|
export type MergeResult = MergeResultOk | MergeResultConflict;
|
|
63
63
|
|
|
64
|
+
// ============================================================================
|
|
65
|
+
// Identity Types
|
|
66
|
+
// ============================================================================
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Canonical identity base shape used across Syncular auth-bound APIs.
|
|
70
|
+
*/
|
|
71
|
+
export interface SyncIdentityBase {
|
|
72
|
+
actorId: string;
|
|
73
|
+
}
|
|
74
|
+
|
|
64
75
|
// ============================================================================
|
|
65
76
|
// Transport Types
|
|
66
77
|
// ============================================================================
|
|
@@ -71,6 +82,7 @@ export type MergeResult = MergeResultOk | MergeResultConflict;
|
|
|
71
82
|
*/
|
|
72
83
|
export type SyncAuthOperation =
|
|
73
84
|
| 'sync'
|
|
85
|
+
| 'authLeaseIssue'
|
|
74
86
|
| 'snapshotChunk'
|
|
75
87
|
| 'snapshotChunkStream'
|
|
76
88
|
| 'blobInitiateUpload'
|
|
@@ -107,12 +119,6 @@ export interface SyncAuthLifecycle {
|
|
|
107
119
|
}
|
|
108
120
|
|
|
109
121
|
export interface SyncTransportOptions {
|
|
110
|
-
/**
|
|
111
|
-
* Legacy per-call auth retry callback.
|
|
112
|
-
* Return true to retry once after refreshing auth.
|
|
113
|
-
* If provided, this takes precedence over `authLifecycle`.
|
|
114
|
-
*/
|
|
115
|
-
onAuthError?: () => Promise<boolean>;
|
|
116
122
|
/**
|
|
117
123
|
* First-class auth lifecycle callbacks.
|
|
118
124
|
* Use this to centralize auth refresh behavior.
|
|
@@ -124,6 +130,45 @@ export interface SyncTransportOptions {
|
|
|
124
130
|
signal?: AbortSignal;
|
|
125
131
|
}
|
|
126
132
|
|
|
133
|
+
export type SyncSnapshotChunkReadMode = 'stream' | 'bytes';
|
|
134
|
+
|
|
135
|
+
export type SyncGzipDecompressionMode = 'stream' | 'buffered';
|
|
136
|
+
|
|
137
|
+
export type SyncBootstrapApplyMode = 'single-transaction' | 'per-subscription';
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Declares transport/runtime capabilities that affect how the client should
|
|
141
|
+
* fetch, decode, and apply bootstrap snapshots.
|
|
142
|
+
*/
|
|
143
|
+
export interface SyncTransportCapabilities {
|
|
144
|
+
/**
|
|
145
|
+
* Preferred snapshot chunk download mode for this runtime.
|
|
146
|
+
* - `stream`: consume the response body as a stream when available
|
|
147
|
+
* - `bytes`: buffer the whole response body before decode/apply
|
|
148
|
+
*/
|
|
149
|
+
snapshotChunkReadMode?: SyncSnapshotChunkReadMode;
|
|
150
|
+
/**
|
|
151
|
+
* Preferred gzip decode mode for this runtime.
|
|
152
|
+
* - `stream`: use `DecompressionStream`
|
|
153
|
+
* - `buffered`: gunzip a materialized byte buffer
|
|
154
|
+
*/
|
|
155
|
+
gzipDecompressionMode?: SyncGzipDecompressionMode;
|
|
156
|
+
/**
|
|
157
|
+
* Hint that chunked bootstrap snapshots should be materialized into rows
|
|
158
|
+
* before plugin/app consumption.
|
|
159
|
+
*/
|
|
160
|
+
preferMaterializedSnapshots?: boolean;
|
|
161
|
+
/**
|
|
162
|
+
* Preferred default transaction strategy for bootstrap apply.
|
|
163
|
+
*/
|
|
164
|
+
preferredBootstrapApplyMode?: SyncBootstrapApplyMode;
|
|
165
|
+
/**
|
|
166
|
+
* Preferred scheduler yield delay (ms) between bootstrap snapshot apply
|
|
167
|
+
* batches. `0` yields on the next macrotask, `false` disables yielding.
|
|
168
|
+
*/
|
|
169
|
+
preferredSnapshotApplyYieldMs?: number | false;
|
|
170
|
+
}
|
|
171
|
+
|
|
127
172
|
/**
|
|
128
173
|
* Blob transport operations (optional extension to SyncTransport).
|
|
129
174
|
* When present, enables blob upload/download through the same transport.
|
|
@@ -163,6 +208,10 @@ export interface SyncTransportBlobs {
|
|
|
163
208
|
* Transport interface for sync operations.
|
|
164
209
|
*/
|
|
165
210
|
export interface SyncTransport {
|
|
211
|
+
/**
|
|
212
|
+
* Optional runtime/transport capabilities.
|
|
213
|
+
*/
|
|
214
|
+
capabilities?: SyncTransportCapabilities;
|
|
166
215
|
/**
|
|
167
216
|
* Combined push+pull in a single round-trip.
|
|
168
217
|
*/
|
|
@@ -175,7 +224,10 @@ export interface SyncTransport {
|
|
|
175
224
|
* Download an encoded bootstrap snapshot chunk.
|
|
176
225
|
*/
|
|
177
226
|
fetchSnapshotChunk(
|
|
178
|
-
request: {
|
|
227
|
+
request: {
|
|
228
|
+
chunkId: string;
|
|
229
|
+
scopeValues?: Record<string, string | string[]>;
|
|
230
|
+
},
|
|
179
231
|
options?: SyncTransportOptions
|
|
180
232
|
): Promise<Uint8Array>;
|
|
181
233
|
|
|
@@ -186,7 +238,10 @@ export interface SyncTransport {
|
|
|
186
238
|
* incrementally without materializing the entire chunk in memory.
|
|
187
239
|
*/
|
|
188
240
|
fetchSnapshotChunkStream?(
|
|
189
|
-
request: {
|
|
241
|
+
request: {
|
|
242
|
+
chunkId: string;
|
|
243
|
+
scopeValues?: Record<string, string | string[]>;
|
|
244
|
+
},
|
|
190
245
|
options?: SyncTransportOptions
|
|
191
246
|
): Promise<ReadableStream<Uint8Array>>;
|
|
192
247
|
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { getBunRuntime } from './internal-runtime';
|
|
2
|
+
|
|
3
|
+
export function bytesToReadableStream(
|
|
4
|
+
bytes: Uint8Array
|
|
5
|
+
): ReadableStream<Uint8Array> {
|
|
6
|
+
return new ReadableStream<Uint8Array>({
|
|
7
|
+
start(controller) {
|
|
8
|
+
controller.enqueue(bytes);
|
|
9
|
+
controller.close();
|
|
10
|
+
},
|
|
11
|
+
});
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function concatByteChunks(chunks: readonly Uint8Array[]): Uint8Array {
|
|
15
|
+
if (chunks.length === 0) {
|
|
16
|
+
return new Uint8Array();
|
|
17
|
+
}
|
|
18
|
+
if (chunks.length === 1) {
|
|
19
|
+
return chunks[0] ?? new Uint8Array();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let total = 0;
|
|
23
|
+
for (const chunk of chunks) {
|
|
24
|
+
total += chunk.length;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const merged = new Uint8Array(total);
|
|
28
|
+
let offset = 0;
|
|
29
|
+
for (const chunk of chunks) {
|
|
30
|
+
merged.set(chunk, offset);
|
|
31
|
+
offset += chunk.length;
|
|
32
|
+
}
|
|
33
|
+
return merged;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function readAllBytesFromStream(
|
|
37
|
+
stream: ReadableStream<Uint8Array>
|
|
38
|
+
): Promise<Uint8Array> {
|
|
39
|
+
const bun = getBunRuntime();
|
|
40
|
+
if (bun?.readableStreamToBytes) {
|
|
41
|
+
const bytes = await bun.readableStreamToBytes(stream);
|
|
42
|
+
return bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const reader = stream.getReader();
|
|
46
|
+
const chunks: Uint8Array[] = [];
|
|
47
|
+
let total = 0;
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
while (true) {
|
|
51
|
+
const { done, value } = await reader.read();
|
|
52
|
+
if (done) break;
|
|
53
|
+
if (!value) continue;
|
|
54
|
+
chunks.push(value);
|
|
55
|
+
total += value.length;
|
|
56
|
+
}
|
|
57
|
+
} finally {
|
|
58
|
+
reader.releaseLock();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (chunks.length === 0) {
|
|
62
|
+
return new Uint8Array();
|
|
63
|
+
}
|
|
64
|
+
if (chunks.length === 1) {
|
|
65
|
+
return chunks[0] ?? new Uint8Array();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const merged = new Uint8Array(total);
|
|
69
|
+
let offset = 0;
|
|
70
|
+
for (const chunk of chunks) {
|
|
71
|
+
merged.set(chunk, offset);
|
|
72
|
+
offset += chunk.length;
|
|
73
|
+
}
|
|
74
|
+
return merged;
|
|
75
|
+
}
|
package/src/utils/compression.ts
CHANGED
|
@@ -1,68 +1,160 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
1
|
+
import { gunzipSync as gunzipSyncFflate } from 'fflate';
|
|
2
|
+
import { bytesToReadableStream, readAllBytesFromStream } from './bytes';
|
|
3
|
+
import { getBunRuntime, usesNodeRuntimeModules } from './internal-runtime';
|
|
4
|
+
|
|
5
|
+
type CompressionResult = Uint8Array | ArrayBuffer | ArrayLike<number>;
|
|
6
|
+
|
|
7
|
+
interface NodeZlibModule {
|
|
8
|
+
gzip(
|
|
9
|
+
payload: Uint8Array,
|
|
10
|
+
callback: (error: Error | null, compressed: CompressionResult) => void
|
|
11
|
+
): void;
|
|
12
|
+
gzip(
|
|
13
|
+
payload: Uint8Array,
|
|
14
|
+
options: { level?: number },
|
|
15
|
+
callback: (error: Error | null, compressed: CompressionResult) => void
|
|
16
|
+
): void;
|
|
17
|
+
gunzip(
|
|
18
|
+
payload: Uint8Array,
|
|
19
|
+
callback: (error: Error | null, decompressed: CompressionResult) => void
|
|
20
|
+
): void;
|
|
8
21
|
}
|
|
9
22
|
|
|
10
|
-
|
|
11
|
-
stream: ReadableStream<Uint8Array>
|
|
12
|
-
): Promise<Uint8Array> {
|
|
13
|
-
const reader = stream.getReader();
|
|
14
|
-
const chunks: Uint8Array[] = [];
|
|
15
|
-
let total = 0;
|
|
23
|
+
let nodeZlibModulePromise: Promise<NodeZlibModule | null> | null = null;
|
|
16
24
|
|
|
25
|
+
export interface GzipBytesOptions {
|
|
26
|
+
level?: number;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function importNodeModule(specifier: string): Promise<object> {
|
|
30
|
+
return new Function('specifier', 'return import(specifier);')(
|
|
31
|
+
specifier
|
|
32
|
+
) as Promise<object>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function tryImportNodeModule(specifier: string): Promise<object | null> {
|
|
17
36
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
if (!value) continue;
|
|
22
|
-
chunks.push(value);
|
|
23
|
-
total += value.length;
|
|
24
|
-
}
|
|
25
|
-
} finally {
|
|
26
|
-
reader.releaseLock();
|
|
37
|
+
return importNodeModule(specifier);
|
|
38
|
+
} catch {
|
|
39
|
+
return Promise.resolve(null);
|
|
27
40
|
}
|
|
41
|
+
}
|
|
28
42
|
|
|
29
|
-
|
|
30
|
-
if (
|
|
43
|
+
function isNodeZlibModule(module: object | null): module is NodeZlibModule {
|
|
44
|
+
if (!module) {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const candidate = module as Partial<NodeZlibModule>;
|
|
49
|
+
return (
|
|
50
|
+
typeof candidate.gzip === 'function' &&
|
|
51
|
+
typeof candidate.gunzip === 'function'
|
|
52
|
+
);
|
|
53
|
+
}
|
|
31
54
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
merged.set(chunk, offset);
|
|
36
|
-
offset += chunk.length;
|
|
55
|
+
function toUint8Array(value: CompressionResult): Uint8Array {
|
|
56
|
+
if (value instanceof Uint8Array) {
|
|
57
|
+
return value;
|
|
37
58
|
}
|
|
38
|
-
|
|
59
|
+
if (value instanceof ArrayBuffer) {
|
|
60
|
+
return new Uint8Array(value);
|
|
61
|
+
}
|
|
62
|
+
return Uint8Array.from(value);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
async function getNodeZlibModule(): Promise<NodeZlibModule | null> {
|
|
66
|
+
if (!usesNodeRuntimeModules()) {
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
if (!nodeZlibModulePromise) {
|
|
70
|
+
nodeZlibModulePromise = tryImportNodeModule('node:zlib')
|
|
71
|
+
.then((module) => (isNodeZlibModule(module) ? module : null))
|
|
72
|
+
.catch(() => null);
|
|
73
|
+
}
|
|
74
|
+
return nodeZlibModulePromise;
|
|
39
75
|
}
|
|
40
76
|
|
|
41
77
|
/**
|
|
42
|
-
* Gzip-compress a byte array using
|
|
43
|
-
*
|
|
78
|
+
* Gzip-compress a byte array using the fastest native implementation available
|
|
79
|
+
* in the current runtime.
|
|
44
80
|
*/
|
|
45
|
-
export async function gzipBytes(
|
|
81
|
+
export async function gzipBytes(
|
|
82
|
+
payload: Uint8Array,
|
|
83
|
+
options: GzipBytesOptions = {}
|
|
84
|
+
): Promise<Uint8Array> {
|
|
85
|
+
const bun = getBunRuntime();
|
|
86
|
+
if (bun?.gzipSync) {
|
|
87
|
+
return bun.gzipSync(payload, options);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const nodeZlib = await getNodeZlibModule();
|
|
91
|
+
if (nodeZlib?.gzip) {
|
|
92
|
+
return await new Promise<Uint8Array>((resolve, reject) => {
|
|
93
|
+
const callback = (error: Error | null, compressed: CompressionResult) => {
|
|
94
|
+
if (error) {
|
|
95
|
+
reject(error);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
resolve(toUint8Array(compressed));
|
|
99
|
+
};
|
|
100
|
+
if (options.level !== undefined) {
|
|
101
|
+
nodeZlib.gzip(payload, { level: options.level }, callback);
|
|
102
|
+
} else {
|
|
103
|
+
nodeZlib.gzip(payload, callback);
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
46
108
|
if (typeof CompressionStream !== 'undefined') {
|
|
47
109
|
const stream = bytesToReadableStream(payload).pipeThrough(
|
|
48
|
-
new CompressionStream('gzip') as
|
|
49
|
-
Uint8Array,
|
|
50
|
-
Uint8Array
|
|
51
|
-
>
|
|
110
|
+
new CompressionStream('gzip') as TransformStream<Uint8Array, Uint8Array>
|
|
52
111
|
);
|
|
53
|
-
return
|
|
112
|
+
return readAllBytesFromStream(stream);
|
|
54
113
|
}
|
|
55
114
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
115
|
+
throw new Error(
|
|
116
|
+
'Failed to gzip bytes, no compression implementation available'
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Gzip-decompress a byte array using the fastest implementation available in
|
|
122
|
+
* the current runtime, with a pure-JS fallback for runtimes like Expo/Hermes.
|
|
123
|
+
*/
|
|
124
|
+
export async function gunzipBytes(payload: Uint8Array): Promise<Uint8Array> {
|
|
125
|
+
const bun = getBunRuntime();
|
|
126
|
+
if (bun?.gunzipSync) {
|
|
127
|
+
return bun.gunzipSync(payload);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const nodeZlib = await getNodeZlibModule();
|
|
131
|
+
if (nodeZlib?.gunzip) {
|
|
132
|
+
return await new Promise<Uint8Array>((resolve, reject) => {
|
|
133
|
+
nodeZlib.gunzip(payload, (error, decompressed) => {
|
|
134
|
+
if (error) {
|
|
135
|
+
reject(error);
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
resolve(toUint8Array(decompressed));
|
|
139
|
+
});
|
|
64
140
|
});
|
|
65
|
-
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (typeof DecompressionStream !== 'undefined') {
|
|
144
|
+
const stream = bytesToReadableStream(payload).pipeThrough(
|
|
145
|
+
new DecompressionStream('gzip') as TransformStream<Uint8Array, Uint8Array>
|
|
146
|
+
);
|
|
147
|
+
return readAllBytesFromStream(stream);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
try {
|
|
151
|
+
return gunzipSyncFflate(payload);
|
|
152
|
+
} catch (error) {
|
|
153
|
+
throw new Error(
|
|
154
|
+
'Failed to gunzip bytes, no decompression implementation available',
|
|
155
|
+
{ cause: error }
|
|
156
|
+
);
|
|
157
|
+
}
|
|
66
158
|
}
|
|
67
159
|
|
|
68
160
|
/**
|
|
@@ -73,11 +165,15 @@ export async function gzipBytesToStream(payload: Uint8Array): Promise<{
|
|
|
73
165
|
stream: ReadableStream<Uint8Array>;
|
|
74
166
|
byteLength?: number;
|
|
75
167
|
}> {
|
|
76
|
-
|
|
168
|
+
const bun = getBunRuntime();
|
|
169
|
+
const nodeZlib = await getNodeZlibModule();
|
|
170
|
+
|
|
171
|
+
if (!bun?.gzipSync && !nodeZlib && typeof CompressionStream !== 'undefined') {
|
|
77
172
|
const source = bytesToReadableStream(payload);
|
|
78
|
-
const gzipStream = new CompressionStream(
|
|
79
|
-
|
|
80
|
-
|
|
173
|
+
const gzipStream = new CompressionStream('gzip') as TransformStream<
|
|
174
|
+
Uint8Array,
|
|
175
|
+
Uint8Array
|
|
176
|
+
>;
|
|
81
177
|
return {
|
|
82
178
|
stream: source.pipeThrough(gzipStream),
|
|
83
179
|
};
|
package/src/utils/crypto.ts
CHANGED
|
@@ -1,32 +1,145 @@
|
|
|
1
|
+
import { concatByteChunks } from './bytes';
|
|
2
|
+
import { getBunRuntime, usesNodeRuntimeModules } from './internal-runtime';
|
|
3
|
+
|
|
1
4
|
const textEncoder = new TextEncoder();
|
|
2
5
|
|
|
6
|
+
interface NodeHash {
|
|
7
|
+
update(data: string | Uint8Array): NodeHash;
|
|
8
|
+
digest(encoding: 'hex'): string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface NodeCryptoModule {
|
|
12
|
+
createHash(algorithm: string): NodeHash;
|
|
13
|
+
}
|
|
14
|
+
|
|
3
15
|
function toHex(bytes: Uint8Array): string {
|
|
4
16
|
return Array.from(bytes)
|
|
5
17
|
.map((byte) => byte.toString(16).padStart(2, '0'))
|
|
6
18
|
.join('');
|
|
7
19
|
}
|
|
8
20
|
|
|
21
|
+
function toDigestBufferSource(payload: Uint8Array): Uint8Array<ArrayBuffer> {
|
|
22
|
+
if (payload.buffer instanceof ArrayBuffer) {
|
|
23
|
+
return new Uint8Array(
|
|
24
|
+
payload.buffer,
|
|
25
|
+
payload.byteOffset,
|
|
26
|
+
payload.byteLength
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const owned = new Uint8Array(payload.byteLength);
|
|
31
|
+
owned.set(payload);
|
|
32
|
+
return owned;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let nodeCryptoModulePromise: Promise<NodeCryptoModule | null> | null = null;
|
|
36
|
+
|
|
37
|
+
function importNodeModule(specifier: string): Promise<object> {
|
|
38
|
+
return new Function('specifier', 'return import(specifier);')(
|
|
39
|
+
specifier
|
|
40
|
+
) as Promise<object>;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function tryImportNodeModule(specifier: string): Promise<object | null> {
|
|
44
|
+
try {
|
|
45
|
+
return importNodeModule(specifier);
|
|
46
|
+
} catch {
|
|
47
|
+
return Promise.resolve(null);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function isNodeCryptoModule(module: object | null): module is NodeCryptoModule {
|
|
52
|
+
if (!module) {
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
return typeof (module as Partial<NodeCryptoModule>).createHash === 'function';
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
async function getNodeCryptoModule(): Promise<NodeCryptoModule | null> {
|
|
59
|
+
if (!usesNodeRuntimeModules()) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
if (!nodeCryptoModulePromise) {
|
|
63
|
+
nodeCryptoModulePromise = tryImportNodeModule('node:crypto')
|
|
64
|
+
.then((module) => (isNodeCryptoModule(module) ? module : null))
|
|
65
|
+
.catch(() => null);
|
|
66
|
+
}
|
|
67
|
+
return nodeCryptoModulePromise;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface IncrementalSha256 {
|
|
71
|
+
update(chunk: Uint8Array): void;
|
|
72
|
+
digestHex(): Promise<string>;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export async function createIncrementalSha256(): Promise<IncrementalSha256> {
|
|
76
|
+
const bun = getBunRuntime();
|
|
77
|
+
if (bun?.CryptoHasher) {
|
|
78
|
+
const hasher = new bun.CryptoHasher('sha256');
|
|
79
|
+
return {
|
|
80
|
+
update(chunk) {
|
|
81
|
+
hasher.update(chunk);
|
|
82
|
+
},
|
|
83
|
+
async digestHex() {
|
|
84
|
+
return hasher.digest('hex');
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const nodeCrypto = await getNodeCryptoModule();
|
|
90
|
+
if (nodeCrypto?.createHash) {
|
|
91
|
+
const hasher = nodeCrypto.createHash('sha256');
|
|
92
|
+
return {
|
|
93
|
+
update(chunk) {
|
|
94
|
+
hasher.update(chunk);
|
|
95
|
+
},
|
|
96
|
+
async digestHex() {
|
|
97
|
+
return hasher.digest('hex');
|
|
98
|
+
},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const chunks: Uint8Array[] = [];
|
|
103
|
+
return {
|
|
104
|
+
update(chunk) {
|
|
105
|
+
if (chunk.length === 0) return;
|
|
106
|
+
chunks.push(chunk.slice());
|
|
107
|
+
},
|
|
108
|
+
async digestHex() {
|
|
109
|
+
return sha256Hex(concatByteChunks(chunks));
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
9
114
|
/**
|
|
10
115
|
* Cross-runtime SHA-256 digest helper.
|
|
11
116
|
*
|
|
12
|
-
* Uses
|
|
117
|
+
* Uses native Bun/Node implementations on server runtimes, with Web Crypto
|
|
118
|
+
* fallback for browser and worker environments.
|
|
13
119
|
*/
|
|
14
120
|
export async function sha256Hex(input: string | Uint8Array): Promise<string> {
|
|
15
121
|
const payload = typeof input === 'string' ? textEncoder.encode(input) : input;
|
|
16
122
|
|
|
123
|
+
const bun = getBunRuntime();
|
|
124
|
+
if (bun?.CryptoHasher) {
|
|
125
|
+
const hasher = new bun.CryptoHasher('sha256');
|
|
126
|
+
hasher.update(payload);
|
|
127
|
+
return hasher.digest('hex');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const nodeCrypto = await getNodeCryptoModule();
|
|
131
|
+
if (nodeCrypto?.createHash) {
|
|
132
|
+
return nodeCrypto.createHash('sha256').update(payload).digest('hex');
|
|
133
|
+
}
|
|
134
|
+
|
|
17
135
|
if (typeof crypto !== 'undefined' && crypto.subtle) {
|
|
18
136
|
const digestBuffer = await crypto.subtle.digest(
|
|
19
137
|
'SHA-256',
|
|
20
|
-
payload
|
|
138
|
+
toDigestBufferSource(payload)
|
|
21
139
|
);
|
|
22
140
|
return toHex(new Uint8Array(digestBuffer));
|
|
23
141
|
}
|
|
24
142
|
|
|
25
|
-
try {
|
|
26
|
-
const nodeCrypto = await import('node:crypto');
|
|
27
|
-
return nodeCrypto.createHash('sha256').update(payload).digest('hex');
|
|
28
|
-
} catch {}
|
|
29
|
-
|
|
30
143
|
throw new Error(
|
|
31
144
|
'Failed to create SHA-256 hash, no crypto implementation available'
|
|
32
145
|
);
|
package/src/utils/id.ts
CHANGED
|
@@ -1,5 +1,15 @@
|
|
|
1
1
|
export function generateId(): string {
|
|
2
|
-
|
|
2
|
+
type SyncularCryptoSource = {
|
|
3
|
+
randomUUID?: () => string;
|
|
4
|
+
getRandomValues?: <T extends ArrayBufferView>(array: T) => T;
|
|
5
|
+
};
|
|
6
|
+
const globalWithCrypto = globalThis as typeof globalThis & {
|
|
7
|
+
crypto?: SyncularCryptoSource;
|
|
8
|
+
};
|
|
9
|
+
const cryptoObj: SyncularCryptoSource | undefined =
|
|
10
|
+
typeof crypto !== 'undefined'
|
|
11
|
+
? (crypto as SyncularCryptoSource)
|
|
12
|
+
: globalWithCrypto.crypto;
|
|
3
13
|
if (cryptoObj && typeof cryptoObj.randomUUID === 'function') {
|
|
4
14
|
return cryptoObj.randomUUID();
|
|
5
15
|
}
|
package/src/utils/index.ts
CHANGED
|
@@ -1,4 +1,9 @@
|
|
|
1
|
+
export * from './bytes';
|
|
1
2
|
export * from './compression';
|
|
2
3
|
export * from './crypto';
|
|
3
4
|
export * from './id';
|
|
4
5
|
export * from './object';
|
|
6
|
+
export * from './realtime-change-index';
|
|
7
|
+
export * from './realtime-connection-registry';
|
|
8
|
+
export * from './table-registry';
|
|
9
|
+
export * from './url';
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
interface BunCryptoHasher {
|
|
2
|
+
update(data: string | Uint8Array): void;
|
|
3
|
+
digest(encoding: 'hex'): string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
interface BunRuntime {
|
|
7
|
+
CryptoHasher?: new (algorithm: string) => BunCryptoHasher;
|
|
8
|
+
gzipSync?: (data: Uint8Array, options?: { level?: number }) => Uint8Array;
|
|
9
|
+
gunzipSync?: (data: Uint8Array) => Uint8Array;
|
|
10
|
+
readableStreamToBytes?: (
|
|
11
|
+
stream: ReadableStream<Uint8Array>
|
|
12
|
+
) => Promise<Uint8Array | ArrayBuffer>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
type RuntimeGlobals = typeof globalThis & {
|
|
16
|
+
Bun?: BunRuntime;
|
|
17
|
+
Deno?: object;
|
|
18
|
+
process?: {
|
|
19
|
+
versions?: {
|
|
20
|
+
node?: string;
|
|
21
|
+
};
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
function getRuntimeGlobals(): RuntimeGlobals {
|
|
26
|
+
return globalThis as RuntimeGlobals;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function getBunRuntime(): BunRuntime | null {
|
|
30
|
+
return getRuntimeGlobals().Bun ?? null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function usesNodeRuntimeModules(): boolean {
|
|
34
|
+
const globals = getRuntimeGlobals();
|
|
35
|
+
if (globals.Deno !== undefined) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
return typeof globals.process?.versions?.node === 'string';
|
|
39
|
+
}
|