@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.
Files changed (156) hide show
  1. package/dist/blobs.d.ts +14 -7
  2. package/dist/blobs.d.ts.map +1 -1
  3. package/dist/blobs.js.map +1 -1
  4. package/dist/column-codecs.d.ts +3 -0
  5. package/dist/column-codecs.d.ts.map +1 -1
  6. package/dist/column-codecs.js +32 -4
  7. package/dist/column-codecs.js.map +1 -1
  8. package/dist/conflict.js.map +1 -1
  9. package/dist/database.js.map +1 -1
  10. package/dist/error-responses.d.ts +393 -0
  11. package/dist/error-responses.d.ts.map +1 -0
  12. package/dist/error-responses.js +393 -0
  13. package/dist/error-responses.js.map +1 -0
  14. package/dist/http/api-client.d.ts +11 -0
  15. package/dist/http/api-client.d.ts.map +1 -0
  16. package/dist/http/api-client.js +30 -0
  17. package/dist/http/api-client.js.map +1 -0
  18. package/dist/http/blob.d.ts +6 -0
  19. package/dist/http/blob.d.ts.map +1 -0
  20. package/dist/http/blob.js +46 -0
  21. package/dist/http/blob.js.map +1 -0
  22. package/dist/http/generated/api.d.ts +5241 -0
  23. package/dist/http/generated/api.d.ts.map +1 -0
  24. package/dist/http/generated/api.js +6 -0
  25. package/dist/http/generated/api.js.map +1 -0
  26. package/dist/http/index.d.ts +18 -0
  27. package/dist/http/index.d.ts.map +1 -0
  28. package/dist/http/index.js +212 -0
  29. package/dist/http/index.js.map +1 -0
  30. package/dist/http/shared.d.ts +45 -0
  31. package/dist/http/shared.d.ts.map +1 -0
  32. package/dist/http/shared.js +122 -0
  33. package/dist/http/shared.js.map +1 -0
  34. package/dist/http/transport-client.d.ts +31 -0
  35. package/dist/http/transport-client.d.ts.map +1 -0
  36. package/dist/http/transport-client.js +193 -0
  37. package/dist/http/transport-client.js.map +1 -0
  38. package/dist/index.d.ts +2 -0
  39. package/dist/index.d.ts.map +1 -1
  40. package/dist/index.js +4 -0
  41. package/dist/index.js.map +1 -1
  42. package/dist/kysely-column-codecs.d.ts +17 -1
  43. package/dist/kysely-column-codecs.d.ts.map +1 -1
  44. package/dist/kysely-column-codecs.js +56 -70
  45. package/dist/kysely-column-codecs.js.map +1 -1
  46. package/dist/logger.js.map +1 -1
  47. package/dist/schemas/blobs.d.ts +7 -1
  48. package/dist/schemas/blobs.d.ts.map +1 -1
  49. package/dist/schemas/blobs.js +1 -0
  50. package/dist/schemas/blobs.js.map +1 -1
  51. package/dist/schemas/common.d.ts +73 -1
  52. package/dist/schemas/common.d.ts.map +1 -1
  53. package/dist/schemas/common.js +38 -0
  54. package/dist/schemas/common.js.map +1 -1
  55. package/dist/schemas/sync.d.ts +712 -38
  56. package/dist/schemas/sync.d.ts.map +1 -1
  57. package/dist/schemas/sync.js +209 -5
  58. package/dist/schemas/sync.js.map +1 -1
  59. package/dist/scopes/index.d.ts +5 -0
  60. package/dist/scopes/index.d.ts.map +1 -1
  61. package/dist/scopes/index.js +27 -0
  62. package/dist/scopes/index.js.map +1 -1
  63. package/dist/sentry.d.ts +31 -0
  64. package/dist/sentry.d.ts.map +1 -0
  65. package/dist/sentry.js +147 -0
  66. package/dist/sentry.js.map +1 -0
  67. package/dist/snapshot-chunks.d.ts +104 -20
  68. package/dist/snapshot-chunks.d.ts.map +1 -1
  69. package/dist/snapshot-chunks.js +665 -72
  70. package/dist/snapshot-chunks.js.map +1 -1
  71. package/dist/sync-packs.d.ts +33 -0
  72. package/dist/sync-packs.d.ts.map +1 -0
  73. package/dist/sync-packs.js +821 -0
  74. package/dist/sync-packs.js.map +1 -0
  75. package/dist/telemetry.d.ts.map +1 -1
  76. package/dist/telemetry.js +3 -2
  77. package/dist/telemetry.js.map +1 -1
  78. package/dist/transforms.js.map +1 -1
  79. package/dist/types.d.ts +48 -7
  80. package/dist/types.d.ts.map +1 -1
  81. package/dist/types.js.map +1 -1
  82. package/dist/utils/bytes.d.ts +4 -0
  83. package/dist/utils/bytes.d.ts.map +1 -0
  84. package/dist/utils/bytes.js +66 -0
  85. package/dist/utils/bytes.js.map +1 -0
  86. package/dist/utils/compression.d.ts +11 -3
  87. package/dist/utils/compression.d.ts.map +1 -1
  88. package/dist/utils/compression.js +102 -50
  89. package/dist/utils/compression.js.map +1 -1
  90. package/dist/utils/crypto.d.ts +7 -1
  91. package/dist/utils/crypto.d.ts.map +1 -1
  92. package/dist/utils/crypto.js +89 -7
  93. package/dist/utils/crypto.js.map +1 -1
  94. package/dist/utils/id.d.ts.map +1 -1
  95. package/dist/utils/id.js +4 -1
  96. package/dist/utils/id.js.map +1 -1
  97. package/dist/utils/index.d.ts +5 -0
  98. package/dist/utils/index.d.ts.map +1 -1
  99. package/dist/utils/index.js +5 -0
  100. package/dist/utils/index.js.map +1 -1
  101. package/dist/utils/internal-runtime.d.ts +16 -0
  102. package/dist/utils/internal-runtime.d.ts.map +1 -0
  103. package/dist/utils/internal-runtime.js +14 -0
  104. package/dist/utils/internal-runtime.js.map +1 -0
  105. package/dist/utils/object.js.map +1 -1
  106. package/dist/utils/realtime-change-index.d.ts +9 -0
  107. package/dist/utils/realtime-change-index.d.ts.map +1 -0
  108. package/dist/utils/realtime-change-index.js +36 -0
  109. package/dist/utils/realtime-change-index.js.map +1 -0
  110. package/dist/utils/realtime-connection-registry.d.ts +40 -0
  111. package/dist/utils/realtime-connection-registry.d.ts.map +1 -0
  112. package/dist/utils/realtime-connection-registry.js +237 -0
  113. package/dist/utils/realtime-connection-registry.js.map +1 -0
  114. package/dist/utils/table-registry.d.ts +10 -0
  115. package/dist/utils/table-registry.d.ts.map +1 -0
  116. package/dist/utils/table-registry.js +54 -0
  117. package/dist/utils/table-registry.js.map +1 -0
  118. package/dist/utils/url.d.ts +3 -0
  119. package/dist/utils/url.d.ts.map +1 -0
  120. package/dist/utils/url.js +20 -0
  121. package/dist/utils/url.js.map +1 -0
  122. package/package.json +51 -6
  123. package/src/blobs.ts +24 -7
  124. package/src/column-codecs.ts +36 -8
  125. package/src/error-responses.ts +423 -0
  126. package/src/http/api-client.ts +39 -0
  127. package/src/http/blob.ts +95 -0
  128. package/src/http/generated/api.ts +5241 -0
  129. package/src/http/index.ts +365 -0
  130. package/src/http/shared.ts +220 -0
  131. package/src/http/transport-client.ts +314 -0
  132. package/src/index.ts +4 -0
  133. package/src/kysely-column-codecs.ts +81 -86
  134. package/src/schemas/blobs.ts +10 -1
  135. package/src/schemas/common.ts +44 -0
  136. package/src/schemas/sync.ts +298 -7
  137. package/src/scopes/index.ts +42 -0
  138. package/src/sentry.ts +200 -0
  139. package/src/snapshot-chunks.ts +921 -69
  140. package/src/sync-packs.ts +1096 -0
  141. package/src/telemetry.ts +7 -2
  142. package/src/types.ts +63 -8
  143. package/src/utils/bytes.ts +75 -0
  144. package/src/utils/compression.ts +147 -51
  145. package/src/utils/crypto.ts +120 -7
  146. package/src/utils/id.ts +11 -1
  147. package/src/utils/index.ts +5 -0
  148. package/src/utils/internal-runtime.ts +39 -0
  149. package/src/utils/realtime-change-index.ts +44 -0
  150. package/src/utils/realtime-connection-registry.ts +286 -0
  151. package/src/utils/table-registry.ts +67 -0
  152. package/src/utils/url.ts +29 -0
  153. package/src/__tests__/compression.test.ts +0 -54
  154. package/src/__tests__/conflict.test.ts +0 -325
  155. package/src/__tests__/telemetry.test.ts +0 -170
  156. 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 globalThis.setImmediate === 'function';
124
+ typeof runtimeGlobals.setImmediate === 'function';
120
125
 
121
126
  const defer = isNode
122
- ? (fn: () => void) => globalThis.setImmediate(fn)
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: { chunkId: string },
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: { chunkId: string },
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
+ }
@@ -1,68 +1,160 @@
1
- function bytesToReadableStream(bytes: Uint8Array): ReadableStream<Uint8Array> {
2
- return new ReadableStream<Uint8Array>({
3
- start(controller) {
4
- controller.enqueue(bytes);
5
- controller.close();
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
- async function streamToBytes(
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
- while (true) {
19
- const { done, value } = await reader.read();
20
- if (done) break;
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
- if (chunks.length === 0) return new Uint8Array();
30
- if (chunks.length === 1) return chunks[0] ?? new Uint8Array();
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
- const merged = new Uint8Array(total);
33
- let offset = 0;
34
- for (const chunk of chunks) {
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
- return merged;
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 CompressionStream when available,
43
- * with node:zlib fallback for Node/Bun runtimes.
78
+ * Gzip-compress a byte array using the fastest native implementation available
79
+ * in the current runtime.
44
80
  */
45
- export async function gzipBytes(payload: Uint8Array): Promise<Uint8Array> {
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 unknown as TransformStream<
49
- Uint8Array,
50
- Uint8Array
51
- >
110
+ new CompressionStream('gzip') as TransformStream<Uint8Array, Uint8Array>
52
111
  );
53
- return streamToBytes(stream);
112
+ return readAllBytesFromStream(stream);
54
113
  }
55
114
 
56
- const nodeZlib = await import('node:zlib');
57
- return await new Promise<Uint8Array>((resolve, reject) => {
58
- nodeZlib.gzip(payload, (error, compressed) => {
59
- if (error) {
60
- reject(error);
61
- return;
62
- }
63
- resolve(new Uint8Array(compressed));
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
- if (typeof CompressionStream !== 'undefined') {
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
- 'gzip'
80
- ) as unknown as TransformStream<Uint8Array, Uint8Array>;
173
+ const gzipStream = new CompressionStream('gzip') as TransformStream<
174
+ Uint8Array,
175
+ Uint8Array
176
+ >;
81
177
  return {
82
178
  stream: source.pipeThrough(gzipStream),
83
179
  };
@@ -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 Web Crypto when available, with Node crypto fallback.
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.slice().buffer
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
- const cryptoObj = typeof crypto !== 'undefined' ? crypto : globalThis.crypto;
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
  }
@@ -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
+ }