@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
package/src/snapshot-chunks.ts
CHANGED
|
@@ -2,111 +2,963 @@
|
|
|
2
2
|
* @syncular/core - Snapshot chunk encoding helpers
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
import { sha256Hex } from './utils/crypto';
|
|
6
|
+
|
|
7
|
+
export const SYNC_SNAPSHOT_CHUNK_ENCODING_BINARY_TABLE_V1 = 'binary-table-v1';
|
|
8
|
+
export const SYNC_SNAPSHOT_CHUNK_ENCODINGS = [
|
|
9
|
+
SYNC_SNAPSHOT_CHUNK_ENCODING_BINARY_TABLE_V1,
|
|
10
|
+
] as const;
|
|
11
|
+
export const SYNC_SNAPSHOT_CHUNK_ENCODING =
|
|
12
|
+
SYNC_SNAPSHOT_CHUNK_ENCODING_BINARY_TABLE_V1;
|
|
13
|
+
export type SyncSnapshotChunkEncoding =
|
|
14
|
+
(typeof SYNC_SNAPSHOT_CHUNK_ENCODINGS)[number];
|
|
15
|
+
|
|
16
|
+
export function isSyncSnapshotChunkEncoding(
|
|
17
|
+
value: unknown
|
|
18
|
+
): value is SyncSnapshotChunkEncoding {
|
|
19
|
+
return (
|
|
20
|
+
typeof value === 'string' &&
|
|
21
|
+
(SYNC_SNAPSHOT_CHUNK_ENCODINGS as readonly string[]).includes(value)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
7
24
|
|
|
8
25
|
export const SYNC_SNAPSHOT_CHUNK_COMPRESSION = 'gzip';
|
|
9
26
|
export type SyncSnapshotChunkCompression =
|
|
10
27
|
typeof SYNC_SNAPSHOT_CHUNK_COMPRESSION;
|
|
28
|
+
export const SYNC_SNAPSHOT_ARTIFACT_COMPRESSION_NONE = 'none';
|
|
29
|
+
export const SYNC_SCOPED_SNAPSHOT_ARTIFACT_MANIFEST_VERSION = 1;
|
|
30
|
+
export const SYNC_SCOPED_SNAPSHOT_ARTIFACT_KIND_SQLITE_V1 =
|
|
31
|
+
'sqlite-snapshot-v1';
|
|
32
|
+
export type SyncSnapshotArtifactCompression =
|
|
33
|
+
| SyncSnapshotChunkCompression
|
|
34
|
+
| typeof SYNC_SNAPSHOT_ARTIFACT_COMPRESSION_NONE;
|
|
35
|
+
export type SyncScopedSnapshotArtifactKind =
|
|
36
|
+
typeof SYNC_SCOPED_SNAPSHOT_ARTIFACT_KIND_SQLITE_V1;
|
|
37
|
+
|
|
38
|
+
export interface SnapshotManifestChunkDigestInput {
|
|
39
|
+
id: string;
|
|
40
|
+
byteLength: number;
|
|
41
|
+
sha256: string;
|
|
42
|
+
encoding: SyncSnapshotChunkEncoding;
|
|
43
|
+
compression: SyncSnapshotChunkCompression;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface SnapshotManifestDigestInput {
|
|
47
|
+
version: 1;
|
|
48
|
+
table: string;
|
|
49
|
+
asOfCommitSeq: number;
|
|
50
|
+
scopeDigest: string;
|
|
51
|
+
rowCursor: string | null;
|
|
52
|
+
rowLimit: number;
|
|
53
|
+
nextRowCursor: string | null;
|
|
54
|
+
isFirstPage: boolean;
|
|
55
|
+
isLastPage: boolean;
|
|
56
|
+
chunks: readonly SnapshotManifestChunkDigestInput[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface SnapshotManifestDigestValue
|
|
60
|
+
extends Omit<SnapshotManifestDigestInput, 'chunks'> {
|
|
61
|
+
chunks: SnapshotManifestChunkDigestInput[];
|
|
62
|
+
digest: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ScopedSnapshotArtifactDigestInput {
|
|
66
|
+
version: typeof SYNC_SCOPED_SNAPSHOT_ARTIFACT_MANIFEST_VERSION;
|
|
67
|
+
artifactKind: SyncScopedSnapshotArtifactKind;
|
|
68
|
+
partitionId: string;
|
|
69
|
+
subscriptionId: string;
|
|
70
|
+
table: string;
|
|
71
|
+
schemaVersion: string;
|
|
72
|
+
asOfCommitSeq: number;
|
|
73
|
+
scopeDigest: string;
|
|
74
|
+
rowCursor: string | null;
|
|
75
|
+
rowLimit: number;
|
|
76
|
+
rowCount: number;
|
|
77
|
+
nextRowCursor: string | null;
|
|
78
|
+
isFirstPage: boolean;
|
|
79
|
+
isLastPage: boolean;
|
|
80
|
+
compression: SyncSnapshotArtifactCompression;
|
|
81
|
+
byteLength: number;
|
|
82
|
+
sha256: string;
|
|
83
|
+
featureSet: readonly string[];
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export interface ScopedSnapshotArtifactManifest
|
|
87
|
+
extends ScopedSnapshotArtifactDigestInput {
|
|
88
|
+
featureSet: string[];
|
|
89
|
+
digest: string;
|
|
90
|
+
}
|
|
11
91
|
|
|
12
|
-
const
|
|
13
|
-
const
|
|
92
|
+
const SNAPSHOT_BINARY_TABLE_MAGIC = new Uint8Array([0x53, 0x42, 0x54, 0x31]); // "SBT1"
|
|
93
|
+
const BINARY_TABLE_VERSION = 1;
|
|
94
|
+
const BINARY_TABLE_FLAG_NONE = 0;
|
|
95
|
+
const BINARY_COLUMN_FLAG_NULLABLE = 1;
|
|
14
96
|
const MAX_FRAME_BYTE_LENGTH = 0xffff_ffff;
|
|
97
|
+
const snapshotTextEncoder = new TextEncoder();
|
|
98
|
+
const snapshotTextDecoder = new TextDecoder();
|
|
99
|
+
|
|
100
|
+
export type BinarySnapshotColumnType =
|
|
101
|
+
| 'string'
|
|
102
|
+
| 'integer'
|
|
103
|
+
| 'float'
|
|
104
|
+
| 'boolean'
|
|
105
|
+
| 'json'
|
|
106
|
+
| 'bytes';
|
|
107
|
+
|
|
108
|
+
export interface BinarySnapshotColumn {
|
|
109
|
+
name: string;
|
|
110
|
+
type: BinarySnapshotColumnType;
|
|
111
|
+
nullable?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export interface BinarySnapshotTable {
|
|
115
|
+
table: string;
|
|
116
|
+
columns: readonly BinarySnapshotColumn[];
|
|
117
|
+
rows: readonly Record<string, unknown>[];
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export type BinarySnapshotRowsEncoder<Row = unknown> = (
|
|
121
|
+
rows: readonly Row[]
|
|
122
|
+
) => Uint8Array;
|
|
123
|
+
|
|
124
|
+
export interface DecodedBinarySnapshotTable {
|
|
125
|
+
table: string;
|
|
126
|
+
columns: BinarySnapshotColumn[];
|
|
127
|
+
rows: Record<string, unknown>[];
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const BINARY_TYPE_TAGS = {
|
|
131
|
+
string: 1,
|
|
132
|
+
integer: 2,
|
|
133
|
+
float: 3,
|
|
134
|
+
boolean: 4,
|
|
135
|
+
json: 5,
|
|
136
|
+
bytes: 6,
|
|
137
|
+
} as const satisfies Record<BinarySnapshotColumnType, number>;
|
|
138
|
+
|
|
139
|
+
const BINARY_COLUMN_TYPES_BY_TAG = new Map<number, BinarySnapshotColumnType>(
|
|
140
|
+
Object.entries(BINARY_TYPE_TAGS).map(([type, tag]) => [
|
|
141
|
+
tag,
|
|
142
|
+
type as BinarySnapshotColumnType,
|
|
143
|
+
])
|
|
144
|
+
);
|
|
15
145
|
|
|
16
146
|
function normalizeRowJson(row: unknown): string {
|
|
17
147
|
const serialized = JSON.stringify(row);
|
|
18
148
|
return serialized === undefined ? 'null' : serialized;
|
|
19
149
|
}
|
|
20
150
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
151
|
+
function utf8ByteLength(value: string): number {
|
|
152
|
+
return snapshotTextEncoder.encode(value).length;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function appendStringField(parts: string[], name: string, value: string): void {
|
|
156
|
+
parts.push(`${name}:s:${utf8ByteLength(value)}:${value}`);
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function appendNullableStringField(
|
|
160
|
+
parts: string[],
|
|
161
|
+
name: string,
|
|
162
|
+
value: string | null
|
|
163
|
+
): void {
|
|
164
|
+
if (value === null) {
|
|
165
|
+
parts.push(`${name}:n`);
|
|
166
|
+
return;
|
|
167
|
+
}
|
|
168
|
+
appendStringField(parts, name, value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function appendIntField(parts: string[], name: string, value: number): void {
|
|
172
|
+
if (!Number.isSafeInteger(value)) {
|
|
173
|
+
throw new Error(`Snapshot manifest ${name} must be a safe integer`);
|
|
174
|
+
}
|
|
175
|
+
parts.push(`${name}:i:${value}`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function appendBoolField(parts: string[], name: string, value: boolean): void {
|
|
179
|
+
parts.push(`${name}:b:${value ? 1 : 0}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function snapshotScopeDigestFromCacheKey(scopeKey: string): string {
|
|
183
|
+
const marker = ':scope:';
|
|
184
|
+
const markerIndex = scopeKey.lastIndexOf(marker);
|
|
185
|
+
if (markerIndex < 0) {
|
|
186
|
+
throw new Error('Snapshot scope cache key is missing scope digest');
|
|
187
|
+
}
|
|
188
|
+
const digest = scopeKey.slice(markerIndex + marker.length);
|
|
189
|
+
if (!/^[a-f0-9]{64}$/.test(digest)) {
|
|
190
|
+
throw new Error('Snapshot scope cache key has invalid scope digest');
|
|
191
|
+
}
|
|
192
|
+
return digest;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export function snapshotManifestDigestPayload(
|
|
196
|
+
manifest: SnapshotManifestDigestInput
|
|
197
|
+
): string {
|
|
198
|
+
const parts = ['syncular.snapshot-manifest.v1'];
|
|
199
|
+
appendIntField(parts, 'version', manifest.version);
|
|
200
|
+
appendStringField(parts, 'table', manifest.table);
|
|
201
|
+
appendIntField(parts, 'asOfCommitSeq', manifest.asOfCommitSeq);
|
|
202
|
+
appendStringField(parts, 'scopeDigest', manifest.scopeDigest);
|
|
203
|
+
appendNullableStringField(parts, 'rowCursor', manifest.rowCursor);
|
|
204
|
+
appendIntField(parts, 'rowLimit', manifest.rowLimit);
|
|
205
|
+
appendNullableStringField(parts, 'nextRowCursor', manifest.nextRowCursor);
|
|
206
|
+
appendBoolField(parts, 'isFirstPage', manifest.isFirstPage);
|
|
207
|
+
appendBoolField(parts, 'isLastPage', manifest.isLastPage);
|
|
208
|
+
appendIntField(parts, 'chunkCount', manifest.chunks.length);
|
|
209
|
+
|
|
210
|
+
for (const [index, chunk] of manifest.chunks.entries()) {
|
|
211
|
+
appendIntField(parts, `chunk.${index}.index`, index);
|
|
212
|
+
appendStringField(parts, `chunk.${index}.id`, chunk.id);
|
|
213
|
+
appendIntField(parts, `chunk.${index}.byteLength`, chunk.byteLength);
|
|
214
|
+
appendStringField(parts, `chunk.${index}.sha256`, chunk.sha256);
|
|
215
|
+
appendStringField(parts, `chunk.${index}.encoding`, chunk.encoding);
|
|
216
|
+
appendStringField(parts, `chunk.${index}.compression`, chunk.compression);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return `${parts.join('\n')}\n`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export async function createSnapshotManifestDigest(
|
|
223
|
+
manifest: SnapshotManifestDigestInput
|
|
224
|
+
): Promise<string> {
|
|
225
|
+
return sha256Hex(snapshotManifestDigestPayload(manifest));
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export async function createSnapshotManifest(
|
|
229
|
+
manifest: SnapshotManifestDigestInput
|
|
230
|
+
): Promise<SnapshotManifestDigestValue> {
|
|
231
|
+
return {
|
|
232
|
+
...manifest,
|
|
233
|
+
chunks: [...manifest.chunks],
|
|
234
|
+
digest: await createSnapshotManifestDigest(manifest),
|
|
235
|
+
};
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function normalizedFeatureSet(featureSet: readonly string[]): string[] {
|
|
239
|
+
return Array.from(new Set(featureSet)).sort();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function scopedSnapshotArtifactDigestInput(
|
|
243
|
+
manifest: ScopedSnapshotArtifactDigestInput
|
|
244
|
+
): ScopedSnapshotArtifactDigestInput {
|
|
245
|
+
return {
|
|
246
|
+
...manifest,
|
|
247
|
+
featureSet: normalizedFeatureSet(manifest.featureSet),
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function scopedSnapshotArtifactDigestPayload(
|
|
252
|
+
manifest: ScopedSnapshotArtifactDigestInput
|
|
253
|
+
): string {
|
|
254
|
+
const input = scopedSnapshotArtifactDigestInput(manifest);
|
|
255
|
+
const parts = ['syncular.scoped-snapshot-artifact.v1'];
|
|
256
|
+
appendIntField(parts, 'version', input.version);
|
|
257
|
+
appendStringField(parts, 'artifactKind', input.artifactKind);
|
|
258
|
+
appendStringField(parts, 'partitionId', input.partitionId);
|
|
259
|
+
appendStringField(parts, 'subscriptionId', input.subscriptionId);
|
|
260
|
+
appendStringField(parts, 'table', input.table);
|
|
261
|
+
appendStringField(parts, 'schemaVersion', input.schemaVersion);
|
|
262
|
+
appendIntField(parts, 'asOfCommitSeq', input.asOfCommitSeq);
|
|
263
|
+
appendStringField(parts, 'scopeDigest', input.scopeDigest);
|
|
264
|
+
appendNullableStringField(parts, 'rowCursor', input.rowCursor);
|
|
265
|
+
appendIntField(parts, 'rowLimit', input.rowLimit);
|
|
266
|
+
appendIntField(parts, 'rowCount', input.rowCount);
|
|
267
|
+
appendNullableStringField(parts, 'nextRowCursor', input.nextRowCursor);
|
|
268
|
+
appendBoolField(parts, 'isFirstPage', input.isFirstPage);
|
|
269
|
+
appendBoolField(parts, 'isLastPage', input.isLastPage);
|
|
270
|
+
appendStringField(parts, 'compression', input.compression);
|
|
271
|
+
appendIntField(parts, 'byteLength', input.byteLength);
|
|
272
|
+
appendStringField(parts, 'sha256', input.sha256);
|
|
273
|
+
appendIntField(parts, 'featureCount', input.featureSet.length);
|
|
274
|
+
|
|
275
|
+
for (const [index, feature] of input.featureSet.entries()) {
|
|
276
|
+
appendIntField(parts, `feature.${index}.index`, index);
|
|
277
|
+
appendStringField(parts, `feature.${index}.name`, feature);
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
return `${parts.join('\n')}\n`;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
export async function createScopedSnapshotArtifactDigest(
|
|
284
|
+
manifest: ScopedSnapshotArtifactDigestInput
|
|
285
|
+
): Promise<string> {
|
|
286
|
+
return sha256Hex(scopedSnapshotArtifactDigestPayload(manifest));
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
export async function createScopedSnapshotArtifactManifest(
|
|
290
|
+
manifest: ScopedSnapshotArtifactDigestInput
|
|
291
|
+
): Promise<ScopedSnapshotArtifactManifest> {
|
|
292
|
+
const normalized = scopedSnapshotArtifactDigestInput(manifest);
|
|
293
|
+
return {
|
|
294
|
+
...normalized,
|
|
295
|
+
featureSet: [...normalized.featureSet],
|
|
296
|
+
digest: await createScopedSnapshotArtifactDigest(normalized),
|
|
297
|
+
};
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
export function encodeBinarySnapshotTable(
|
|
301
|
+
table: BinarySnapshotTable
|
|
302
|
+
): Uint8Array {
|
|
303
|
+
const writer = new BinarySnapshotWriter(
|
|
304
|
+
estimateBinarySnapshotTableSize(table)
|
|
305
|
+
);
|
|
306
|
+
const valueWriters = table.columns.map(createBinarySnapshotValueWriter);
|
|
307
|
+
writeBinarySnapshotTableHeader(
|
|
308
|
+
writer,
|
|
309
|
+
table.table,
|
|
310
|
+
table.columns,
|
|
311
|
+
table.rows.length
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
const nullBitmapBytes = Math.ceil(table.columns.length / 8);
|
|
315
|
+
for (const row of table.rows) {
|
|
316
|
+
const nullBitmapOffset = writer.writeZeroes(nullBitmapBytes);
|
|
317
|
+
for (let index = 0; index < table.columns.length; index += 1) {
|
|
318
|
+
const column = table.columns[index]!;
|
|
319
|
+
const value = row[column.name];
|
|
320
|
+
if (value == null) {
|
|
321
|
+
if (!column.nullable) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`Binary snapshot column ${column.name} is not nullable`
|
|
324
|
+
);
|
|
325
|
+
}
|
|
326
|
+
const bitmapIndex = nullBitmapOffset + Math.floor(index / 8);
|
|
327
|
+
writer.patchUint8(
|
|
328
|
+
bitmapIndex,
|
|
329
|
+
writer.readUint8(bitmapIndex) | (1 << (index % 8))
|
|
330
|
+
);
|
|
331
|
+
continue;
|
|
332
|
+
}
|
|
333
|
+
valueWriters[index]!(writer, value);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return writer.toUint8Array();
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
type BinarySnapshotValueWriter = (
|
|
341
|
+
writer: BinarySnapshotWriter,
|
|
342
|
+
value: unknown
|
|
343
|
+
) => void;
|
|
344
|
+
|
|
345
|
+
function createBinarySnapshotValueWriter(
|
|
346
|
+
column: BinarySnapshotColumn
|
|
347
|
+
): BinarySnapshotValueWriter {
|
|
348
|
+
const label = `binary snapshot ${column.name}`;
|
|
349
|
+
switch (column.type) {
|
|
350
|
+
case 'string':
|
|
351
|
+
return (writer, value) => {
|
|
352
|
+
writer.writeString32(binarySnapshotString(value, label), label);
|
|
353
|
+
};
|
|
354
|
+
case 'integer':
|
|
355
|
+
return (writer, value) => {
|
|
356
|
+
writer.writeInt64(value, label);
|
|
357
|
+
};
|
|
358
|
+
case 'float':
|
|
359
|
+
return (writer, value) => {
|
|
360
|
+
if (typeof value !== 'number') {
|
|
361
|
+
throw new Error(`${label} expected number`);
|
|
362
|
+
}
|
|
363
|
+
writer.writeFloat64(value);
|
|
364
|
+
};
|
|
365
|
+
case 'boolean':
|
|
366
|
+
return (writer, value) => {
|
|
367
|
+
if (typeof value !== 'boolean') {
|
|
368
|
+
throw new Error(`${label} expected boolean`);
|
|
369
|
+
}
|
|
370
|
+
writer.writeUint8(value ? 1 : 0);
|
|
371
|
+
};
|
|
372
|
+
case 'json':
|
|
373
|
+
return (writer, value) => {
|
|
374
|
+
writer.writeString32(normalizeRowJson(value), label);
|
|
375
|
+
};
|
|
376
|
+
case 'bytes':
|
|
377
|
+
return (writer, value) => {
|
|
378
|
+
const bytes = coerceBinaryBytes(value, column.name);
|
|
379
|
+
writer.writeUint32(bytes.length);
|
|
380
|
+
writer.writeBytes(bytes);
|
|
381
|
+
};
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
export class BinarySnapshotTableWriter {
|
|
386
|
+
private readonly writer: BinarySnapshotWriter;
|
|
387
|
+
private readonly nullBitmapBytes: number;
|
|
388
|
+
private readonly expectedRows: number;
|
|
389
|
+
private rowsWritten = 0;
|
|
390
|
+
private nullBitmapOffset: number | null = null;
|
|
391
|
+
|
|
392
|
+
constructor(
|
|
393
|
+
table: string,
|
|
394
|
+
private readonly columns: readonly BinarySnapshotColumn[],
|
|
395
|
+
rowCount: number,
|
|
396
|
+
initialCapacity?: number
|
|
397
|
+
) {
|
|
398
|
+
this.expectedRows = rowCount;
|
|
399
|
+
this.nullBitmapBytes = Math.ceil(columns.length / 8);
|
|
400
|
+
this.writer = new BinarySnapshotWriter(
|
|
401
|
+
initialCapacity ??
|
|
402
|
+
estimateBinarySnapshotStaticSize(table, columns, rowCount)
|
|
403
|
+
);
|
|
404
|
+
writeBinarySnapshotTableHeader(this.writer, table, columns, rowCount);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
beginRow(): void {
|
|
408
|
+
if (this.rowsWritten >= this.expectedRows) {
|
|
409
|
+
throw new Error('Binary snapshot writer received too many rows');
|
|
410
|
+
}
|
|
411
|
+
this.nullBitmapOffset = this.writer.writeZeroes(this.nullBitmapBytes);
|
|
412
|
+
this.rowsWritten += 1;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
writeNull(columnIndex: number): void {
|
|
416
|
+
const column = this.column(columnIndex);
|
|
417
|
+
if (!column.nullable) {
|
|
418
|
+
throw new Error(`Binary snapshot column ${column.name} is not nullable`);
|
|
419
|
+
}
|
|
420
|
+
const nullBitmapOffset = this.currentNullBitmapOffset();
|
|
421
|
+
const bitmapIndex = nullBitmapOffset + Math.floor(columnIndex / 8);
|
|
422
|
+
this.writer.patchUint8(
|
|
423
|
+
bitmapIndex,
|
|
424
|
+
this.writer.readUint8(bitmapIndex) | (1 << (columnIndex % 8))
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
writeString(value: string | Date, label: string): void {
|
|
429
|
+
this.currentNullBitmapOffset();
|
|
430
|
+
this.writer.writeString32(binarySnapshotString(value, label), label);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
writeInteger(value: number | bigint | string, label: string): void {
|
|
434
|
+
this.currentNullBitmapOffset();
|
|
435
|
+
this.writer.writeInt64(value, label);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
writeFloat(value: number, label: string): void {
|
|
439
|
+
this.currentNullBitmapOffset();
|
|
440
|
+
if (typeof value !== 'number') {
|
|
441
|
+
throw new Error(`${label} expected number`);
|
|
442
|
+
}
|
|
443
|
+
this.writer.writeFloat64(value);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
writeBoolean(value: boolean, label: string): void {
|
|
447
|
+
this.currentNullBitmapOffset();
|
|
448
|
+
if (typeof value !== 'boolean') {
|
|
449
|
+
throw new Error(`${label} expected boolean`);
|
|
450
|
+
}
|
|
451
|
+
this.writer.writeUint8(value ? 1 : 0);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
writeJson(value: unknown, label: string): void {
|
|
455
|
+
this.currentNullBitmapOffset();
|
|
456
|
+
this.writer.writeString32(normalizeRowJson(value), label);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
writeBytes(value: Uint8Array | ArrayBuffer, columnName: string): void {
|
|
460
|
+
this.currentNullBitmapOffset();
|
|
461
|
+
const bytes = coerceBinaryBytes(value, columnName);
|
|
462
|
+
this.writer.writeUint32(bytes.length);
|
|
463
|
+
this.writer.writeBytes(bytes);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
writeValue(columnIndex: number, value: unknown): void {
|
|
467
|
+
const column = this.column(columnIndex);
|
|
468
|
+
if (value == null) {
|
|
469
|
+
this.writeNull(columnIndex);
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
this.currentNullBitmapOffset();
|
|
473
|
+
writeBinarySnapshotValue(this.writer, column, value);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
finish(): Uint8Array {
|
|
477
|
+
if (this.rowsWritten !== this.expectedRows) {
|
|
32
478
|
throw new Error(
|
|
33
|
-
`
|
|
479
|
+
`Binary snapshot writer expected ${this.expectedRows} rows, received ${this.rowsWritten}`
|
|
34
480
|
);
|
|
35
481
|
}
|
|
36
|
-
|
|
37
|
-
totalByteLength += FRAME_LENGTH_BYTES + payload.length;
|
|
482
|
+
return this.writer.toUint8Array();
|
|
38
483
|
}
|
|
39
484
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
encoded.set(payload, offset);
|
|
47
|
-
offset += payload.length;
|
|
485
|
+
private column(columnIndex: number): BinarySnapshotColumn {
|
|
486
|
+
const column = this.columns[columnIndex];
|
|
487
|
+
if (!column) {
|
|
488
|
+
throw new Error(`Binary snapshot column index ${columnIndex} is invalid`);
|
|
489
|
+
}
|
|
490
|
+
return column;
|
|
48
491
|
}
|
|
49
492
|
|
|
50
|
-
|
|
493
|
+
private currentNullBitmapOffset(): number {
|
|
494
|
+
if (this.nullBitmapOffset == null) {
|
|
495
|
+
throw new Error('Binary snapshot writer has no active row');
|
|
496
|
+
}
|
|
497
|
+
return this.nullBitmapOffset;
|
|
498
|
+
}
|
|
51
499
|
}
|
|
52
500
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
501
|
+
export function decodeBinarySnapshotTable(
|
|
502
|
+
bytes: Uint8Array
|
|
503
|
+
): DecodedBinarySnapshotTable {
|
|
504
|
+
const reader = new BinarySnapshotReader(bytes);
|
|
505
|
+
reader.expectMagic(SNAPSHOT_BINARY_TABLE_MAGIC, 'binary snapshot table');
|
|
506
|
+
const version = reader.readUint16('binary snapshot version');
|
|
507
|
+
if (version !== BINARY_TABLE_VERSION) {
|
|
508
|
+
throw new Error(`Unsupported binary snapshot version: ${version}`);
|
|
509
|
+
}
|
|
510
|
+
const flags = reader.readUint16('binary snapshot flags');
|
|
511
|
+
if (flags !== BINARY_TABLE_FLAG_NONE) {
|
|
512
|
+
throw new Error(`Unsupported binary snapshot flags: ${flags}`);
|
|
513
|
+
}
|
|
65
514
|
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
515
|
+
const table = reader.readString16('binary snapshot table name');
|
|
516
|
+
const columnCount = reader.readUint16('binary snapshot column count');
|
|
517
|
+
const columns: BinarySnapshotColumn[] = [];
|
|
518
|
+
for (let index = 0; index < columnCount; index += 1) {
|
|
519
|
+
const name = reader.readString16('binary snapshot column name');
|
|
520
|
+
const typeTag = reader.readUint8('binary snapshot column type');
|
|
521
|
+
const type = BINARY_COLUMN_TYPES_BY_TAG.get(typeTag);
|
|
522
|
+
if (!type)
|
|
523
|
+
throw new Error(`Unsupported binary snapshot type tag: ${typeTag}`);
|
|
524
|
+
const columnFlags = reader.readUint8('binary snapshot column flags');
|
|
525
|
+
if (columnFlags & ~BINARY_COLUMN_FLAG_NULLABLE) {
|
|
526
|
+
throw new Error(
|
|
527
|
+
`Unsupported binary snapshot column flags: ${columnFlags}`
|
|
528
|
+
);
|
|
529
|
+
}
|
|
530
|
+
columns.push({
|
|
531
|
+
name,
|
|
532
|
+
type,
|
|
533
|
+
...(columnFlags & BINARY_COLUMN_FLAG_NULLABLE ? { nullable: true } : {}),
|
|
534
|
+
});
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
const rowCount = reader.readUint32('binary snapshot row count');
|
|
538
|
+
const rows: Record<string, unknown>[] = [];
|
|
539
|
+
const nullBitmapBytes = Math.ceil(columns.length / 8);
|
|
540
|
+
for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
|
|
541
|
+
const nullBitmap = reader.readBytes(
|
|
542
|
+
nullBitmapBytes,
|
|
543
|
+
'binary snapshot row null bitmap'
|
|
544
|
+
);
|
|
545
|
+
const row: Record<string, unknown> = {};
|
|
546
|
+
for (let columnIndex = 0; columnIndex < columns.length; columnIndex += 1) {
|
|
547
|
+
const column = columns[columnIndex]!;
|
|
548
|
+
const isNull =
|
|
549
|
+
(nullBitmap[Math.floor(columnIndex / 8)]! &
|
|
550
|
+
(1 << (columnIndex % 8))) !==
|
|
551
|
+
0;
|
|
552
|
+
if (isNull && !column.nullable) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`Binary snapshot column ${column.name} is not nullable`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
row[column.name] = isNull
|
|
558
|
+
? null
|
|
559
|
+
: reader.readValue(column.type, `binary snapshot ${column.name}`);
|
|
560
|
+
}
|
|
561
|
+
rows.push(row);
|
|
562
|
+
}
|
|
563
|
+
reader.assertDone();
|
|
69
564
|
|
|
70
|
-
return
|
|
565
|
+
return { table, columns, rows };
|
|
71
566
|
}
|
|
72
567
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
568
|
+
function writeBinarySnapshotValue(
|
|
569
|
+
writer: BinarySnapshotWriter,
|
|
570
|
+
column: BinarySnapshotColumn,
|
|
571
|
+
value: unknown
|
|
572
|
+
): void {
|
|
573
|
+
switch (column.type) {
|
|
574
|
+
case 'string': {
|
|
575
|
+
if (typeof value !== 'string') {
|
|
576
|
+
throw new Error(
|
|
577
|
+
`Binary snapshot column ${column.name} expected string`
|
|
578
|
+
);
|
|
579
|
+
}
|
|
580
|
+
writer.writeString32(value, `binary snapshot ${column.name}`);
|
|
581
|
+
return;
|
|
582
|
+
}
|
|
583
|
+
case 'integer':
|
|
584
|
+
writer.writeInt64(value, `binary snapshot ${column.name}`);
|
|
585
|
+
return;
|
|
586
|
+
case 'float': {
|
|
587
|
+
if (typeof value !== 'number') {
|
|
588
|
+
throw new Error(
|
|
589
|
+
`Binary snapshot column ${column.name} expected number`
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
writer.writeFloat64(value);
|
|
593
|
+
return;
|
|
594
|
+
}
|
|
595
|
+
case 'boolean': {
|
|
596
|
+
if (typeof value !== 'boolean') {
|
|
597
|
+
throw new Error(
|
|
598
|
+
`Binary snapshot column ${column.name} expected boolean`
|
|
599
|
+
);
|
|
600
|
+
}
|
|
601
|
+
writer.writeUint8(value ? 1 : 0);
|
|
602
|
+
return;
|
|
603
|
+
}
|
|
604
|
+
case 'json':
|
|
605
|
+
writer.writeString32(
|
|
606
|
+
normalizeRowJson(value),
|
|
607
|
+
`binary snapshot ${column.name}`
|
|
608
|
+
);
|
|
609
|
+
return;
|
|
610
|
+
case 'bytes': {
|
|
611
|
+
const bytes = coerceBinaryBytes(value, column.name);
|
|
612
|
+
writer.writeUint32(bytes.length);
|
|
613
|
+
writer.writeBytes(bytes);
|
|
614
|
+
return;
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function binarySnapshotInt64(value: unknown, label: string): bigint {
|
|
620
|
+
let bigint: bigint;
|
|
621
|
+
if (typeof value === 'bigint') {
|
|
622
|
+
bigint = value;
|
|
623
|
+
} else if (typeof value === 'number' && Number.isSafeInteger(value)) {
|
|
624
|
+
bigint = BigInt(value);
|
|
625
|
+
} else if (typeof value === 'string' && /^-?\d+$/.test(value)) {
|
|
626
|
+
bigint = BigInt(value);
|
|
627
|
+
} else {
|
|
628
|
+
throw new Error(`${label} expected a safe integer or bigint`);
|
|
629
|
+
}
|
|
630
|
+
return bigint;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
function binarySnapshotString(value: unknown, label: string): string {
|
|
634
|
+
if (typeof value === 'string') return value;
|
|
635
|
+
if (value instanceof Date) return value.toISOString();
|
|
636
|
+
throw new Error(`${label} expected string`);
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function coerceBinaryBytes(value: unknown, columnName: string): Uint8Array {
|
|
640
|
+
if (value instanceof Uint8Array) return value;
|
|
641
|
+
if (value instanceof ArrayBuffer) return new Uint8Array(value);
|
|
642
|
+
throw new Error(`Binary snapshot column ${columnName} expected bytes`);
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
function assertUint16(value: number, label: string): void {
|
|
646
|
+
if (!Number.isInteger(value) || value < 0 || value > 0xffff) {
|
|
647
|
+
throw new Error(`${label} exceeds uint16 bounds`);
|
|
79
648
|
}
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
function assertUint32(value: number, label: string): void {
|
|
652
|
+
if (!Number.isInteger(value) || value < 0 || value > MAX_FRAME_BYTE_LENGTH) {
|
|
653
|
+
throw new Error(`${label} exceeds uint32 bounds`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
function estimateBinarySnapshotTableSize(table: BinarySnapshotTable): number {
|
|
658
|
+
return estimateBinarySnapshotStaticSize(
|
|
659
|
+
table.table,
|
|
660
|
+
table.columns,
|
|
661
|
+
table.rows.length
|
|
662
|
+
);
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function estimateBinarySnapshotStaticSize(
|
|
666
|
+
table: string,
|
|
667
|
+
columns: readonly BinarySnapshotColumn[],
|
|
668
|
+
rowCount: number
|
|
669
|
+
): number {
|
|
670
|
+
const columnHeaderBytes = columns.reduce(
|
|
671
|
+
(sum, column) => sum + 4 + column.name.length * 4,
|
|
672
|
+
0
|
|
673
|
+
);
|
|
674
|
+
const rowOverhead = rowCount * Math.max(1, Math.ceil(columns.length / 8));
|
|
675
|
+
const scalarBytes = rowCount * columns.length * 16;
|
|
676
|
+
return (
|
|
677
|
+
SNAPSHOT_BINARY_TABLE_MAGIC.length +
|
|
678
|
+
10 +
|
|
679
|
+
table.length * 4 +
|
|
680
|
+
columnHeaderBytes +
|
|
681
|
+
rowOverhead +
|
|
682
|
+
scalarBytes
|
|
683
|
+
);
|
|
684
|
+
}
|
|
80
685
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
686
|
+
function writeBinarySnapshotTableHeader(
|
|
687
|
+
writer: BinarySnapshotWriter,
|
|
688
|
+
table: string,
|
|
689
|
+
columns: readonly BinarySnapshotColumn[],
|
|
690
|
+
rowCount: number
|
|
691
|
+
): void {
|
|
692
|
+
assertUint16(columns.length, 'binary snapshot column count');
|
|
693
|
+
assertUint32(rowCount, 'binary snapshot row count');
|
|
694
|
+
|
|
695
|
+
writer.writeBytes(SNAPSHOT_BINARY_TABLE_MAGIC);
|
|
696
|
+
writer.writeUint16(BINARY_TABLE_VERSION);
|
|
697
|
+
writer.writeUint16(BINARY_TABLE_FLAG_NONE);
|
|
698
|
+
writer.writeString16(table, 'binary snapshot table name');
|
|
699
|
+
writer.writeUint16(columns.length);
|
|
700
|
+
|
|
701
|
+
for (const column of columns) {
|
|
702
|
+
writer.writeString16(column.name, 'binary snapshot column name');
|
|
703
|
+
writer.writeUint8(BINARY_TYPE_TAGS[column.type]);
|
|
704
|
+
writer.writeUint8(column.nullable ? BINARY_COLUMN_FLAG_NULLABLE : 0);
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
writer.writeUint32(rowCount);
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
class BinarySnapshotWriter {
|
|
711
|
+
private bytes: Uint8Array;
|
|
712
|
+
private view: DataView;
|
|
713
|
+
private offset = 0;
|
|
714
|
+
|
|
715
|
+
constructor(initialCapacity: number) {
|
|
716
|
+
this.bytes = new Uint8Array(Math.max(64, initialCapacity));
|
|
717
|
+
this.view = new DataView(
|
|
718
|
+
this.bytes.buffer,
|
|
719
|
+
this.bytes.byteOffset,
|
|
720
|
+
this.bytes.length
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
writeUint8(value: number): void {
|
|
725
|
+
this.ensure(1);
|
|
726
|
+
this.view.setUint8(this.offset, value);
|
|
727
|
+
this.offset += 1;
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
writeUint16(value: number): void {
|
|
731
|
+
assertUint16(value, 'binary snapshot uint16');
|
|
732
|
+
this.ensure(2);
|
|
733
|
+
this.view.setUint16(this.offset, value, true);
|
|
734
|
+
this.offset += 2;
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
writeUint32(value: number): void {
|
|
738
|
+
assertUint32(value, 'binary snapshot uint32');
|
|
739
|
+
this.ensure(4);
|
|
740
|
+
this.view.setUint32(this.offset, value, true);
|
|
741
|
+
this.offset += 4;
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
writeInt64(value: unknown, label: string): void {
|
|
745
|
+
this.ensure(8);
|
|
746
|
+
if (typeof value === 'number') {
|
|
747
|
+
if (!Number.isSafeInteger(value)) {
|
|
748
|
+
throw new Error(`${label} expected a safe integer or bigint`);
|
|
749
|
+
}
|
|
750
|
+
if (value >= 0) {
|
|
751
|
+
this.view.setUint32(this.offset, value >>> 0, true);
|
|
752
|
+
this.view.setUint32(
|
|
753
|
+
this.offset + 4,
|
|
754
|
+
Math.floor(value / 0x1_0000_0000),
|
|
755
|
+
true
|
|
756
|
+
);
|
|
757
|
+
} else {
|
|
758
|
+
this.view.setBigInt64(this.offset, BigInt(value), true);
|
|
759
|
+
}
|
|
760
|
+
} else {
|
|
761
|
+
this.view.setBigInt64(
|
|
762
|
+
this.offset,
|
|
763
|
+
binarySnapshotInt64(value, label),
|
|
764
|
+
true
|
|
765
|
+
);
|
|
86
766
|
}
|
|
767
|
+
this.offset += 8;
|
|
87
768
|
}
|
|
88
769
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
770
|
+
writeFloat64(value: number): void {
|
|
771
|
+
this.ensure(8);
|
|
772
|
+
this.view.setFloat64(this.offset, value, true);
|
|
773
|
+
this.offset += 8;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
writeString16(value: string, label: string): void {
|
|
777
|
+
if (this.writeAsciiString16(value, label)) return;
|
|
778
|
+
const bytes = snapshotTextEncoder.encode(value);
|
|
779
|
+
assertUint16(bytes.length, label);
|
|
780
|
+
this.writeUint16(bytes.length);
|
|
781
|
+
this.writeBytes(bytes);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
writeString32(value: string, label: string): void {
|
|
785
|
+
if (this.writeAsciiString32(value, label)) return;
|
|
786
|
+
const bytes = snapshotTextEncoder.encode(value);
|
|
787
|
+
assertUint32(bytes.length, label);
|
|
788
|
+
this.writeUint32(bytes.length);
|
|
789
|
+
this.writeBytes(bytes);
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
writeBytes(bytes: Uint8Array): void {
|
|
793
|
+
this.ensure(bytes.length);
|
|
794
|
+
this.bytes.set(bytes, this.offset);
|
|
795
|
+
this.offset += bytes.length;
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
writeZeroes(length: number): number {
|
|
799
|
+
this.ensure(length);
|
|
800
|
+
const start = this.offset;
|
|
801
|
+
this.bytes.fill(0, start, start + length);
|
|
802
|
+
this.offset += length;
|
|
803
|
+
return start;
|
|
804
|
+
}
|
|
93
805
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
806
|
+
readUint8(offset: number): number {
|
|
807
|
+
return this.view.getUint8(offset);
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
patchUint8(offset: number, value: number): void {
|
|
811
|
+
this.view.setUint8(offset, value);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
toUint8Array(): Uint8Array {
|
|
815
|
+
return this.bytes.subarray(0, this.offset);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
private ensure(length: number): void {
|
|
819
|
+
const required = this.offset + length;
|
|
820
|
+
if (required <= this.bytes.length) return;
|
|
821
|
+
let nextLength = this.bytes.length;
|
|
822
|
+
while (nextLength < required) {
|
|
823
|
+
nextLength *= 2;
|
|
97
824
|
}
|
|
825
|
+
const next = new Uint8Array(nextLength);
|
|
826
|
+
next.set(this.bytes, 0);
|
|
827
|
+
this.bytes = next;
|
|
828
|
+
this.view = new DataView(
|
|
829
|
+
this.bytes.buffer,
|
|
830
|
+
this.bytes.byteOffset,
|
|
831
|
+
this.bytes.length
|
|
832
|
+
);
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
private writeAsciiString16(value: string, label: string): boolean {
|
|
836
|
+
assertUint16(value.length, label);
|
|
837
|
+
const start = this.offset;
|
|
838
|
+
this.writeUint16(value.length);
|
|
839
|
+
return this.tryWriteAsciiBytes(value, start);
|
|
840
|
+
}
|
|
98
841
|
|
|
99
|
-
|
|
100
|
-
|
|
842
|
+
private writeAsciiString32(value: string, label: string): boolean {
|
|
843
|
+
assertUint32(value.length, label);
|
|
844
|
+
const start = this.offset;
|
|
845
|
+
this.writeUint32(value.length);
|
|
846
|
+
return this.tryWriteAsciiBytes(value, start);
|
|
847
|
+
}
|
|
101
848
|
|
|
102
|
-
|
|
103
|
-
|
|
849
|
+
private tryWriteAsciiBytes(value: string, rollbackOffset: number): boolean {
|
|
850
|
+
this.ensure(value.length);
|
|
851
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
852
|
+
const code = value.charCodeAt(index);
|
|
853
|
+
if (code > 0x7f) {
|
|
854
|
+
this.offset = rollbackOffset;
|
|
855
|
+
return false;
|
|
856
|
+
}
|
|
857
|
+
this.bytes[this.offset + index] = code;
|
|
104
858
|
}
|
|
859
|
+
this.offset += value.length;
|
|
860
|
+
return true;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
105
863
|
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
864
|
+
class BinarySnapshotReader {
|
|
865
|
+
private readonly view: DataView;
|
|
866
|
+
private offset = 0;
|
|
867
|
+
|
|
868
|
+
constructor(private readonly bytes: Uint8Array) {
|
|
869
|
+
this.view = new DataView(bytes.buffer, bytes.byteOffset, bytes.length);
|
|
870
|
+
}
|
|
871
|
+
|
|
872
|
+
expectMagic(magic: Uint8Array, label: string): void {
|
|
873
|
+
const actual = this.readBytes(magic.length, `${label} magic`);
|
|
874
|
+
for (let index = 0; index < magic.length; index += 1) {
|
|
875
|
+
if (actual[index] !== magic[index])
|
|
876
|
+
throw new Error(`Unexpected ${label} magic`);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
readUint8(label: string): number {
|
|
881
|
+
this.require(1, label);
|
|
882
|
+
const value = this.view.getUint8(this.offset);
|
|
883
|
+
this.offset += 1;
|
|
884
|
+
return value;
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
readUint16(label: string): number {
|
|
888
|
+
this.require(2, label);
|
|
889
|
+
const value = this.view.getUint16(this.offset, true);
|
|
890
|
+
this.offset += 2;
|
|
891
|
+
return value;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
readUint32(label: string): number {
|
|
895
|
+
this.require(4, label);
|
|
896
|
+
const value = this.view.getUint32(this.offset, true);
|
|
897
|
+
this.offset += 4;
|
|
898
|
+
return value;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
readString16(label: string): string {
|
|
902
|
+
const length = this.readUint16(`${label} length`);
|
|
903
|
+
return snapshotTextDecoder.decode(this.readBytes(length, label));
|
|
109
904
|
}
|
|
110
905
|
|
|
111
|
-
|
|
906
|
+
readString32(label: string): string {
|
|
907
|
+
const length = this.readUint32(`${label} length`);
|
|
908
|
+
return snapshotTextDecoder.decode(this.readBytes(length, label));
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
readBytes(length: number, label: string): Uint8Array {
|
|
912
|
+
this.require(length, label);
|
|
913
|
+
const value = this.bytes.subarray(this.offset, this.offset + length);
|
|
914
|
+
this.offset += length;
|
|
915
|
+
return value;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
readValue(type: BinarySnapshotColumnType, label: string): unknown {
|
|
919
|
+
switch (type) {
|
|
920
|
+
case 'string':
|
|
921
|
+
return this.readString32(label);
|
|
922
|
+
case 'integer': {
|
|
923
|
+
this.require(8, label);
|
|
924
|
+
const value = this.view.getBigInt64(this.offset, true);
|
|
925
|
+
this.offset += 8;
|
|
926
|
+
const numberValue = Number(value);
|
|
927
|
+
return Number.isSafeInteger(numberValue) &&
|
|
928
|
+
BigInt(numberValue) === value
|
|
929
|
+
? numberValue
|
|
930
|
+
: value;
|
|
931
|
+
}
|
|
932
|
+
case 'float': {
|
|
933
|
+
this.require(8, label);
|
|
934
|
+
const value = this.view.getFloat64(this.offset, true);
|
|
935
|
+
this.offset += 8;
|
|
936
|
+
return value;
|
|
937
|
+
}
|
|
938
|
+
case 'boolean': {
|
|
939
|
+
const value = this.readUint8(label);
|
|
940
|
+
if (value !== 0 && value !== 1)
|
|
941
|
+
throw new Error(`${label} expected boolean byte`);
|
|
942
|
+
return value === 1;
|
|
943
|
+
}
|
|
944
|
+
case 'json':
|
|
945
|
+
return JSON.parse(this.readString32(label));
|
|
946
|
+
case 'bytes': {
|
|
947
|
+
const length = this.readUint32(`${label} length`);
|
|
948
|
+
return this.readBytes(length, label);
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
assertDone(): void {
|
|
954
|
+
if (this.offset !== this.bytes.length) {
|
|
955
|
+
throw new Error('Binary snapshot payload has trailing bytes');
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
private require(length: number, label: string): void {
|
|
960
|
+
if (this.offset + length > this.bytes.length) {
|
|
961
|
+
throw new Error(`${label} exceeds binary snapshot payload bounds`);
|
|
962
|
+
}
|
|
963
|
+
}
|
|
112
964
|
}
|