@syncular/core 0.1.3 → 0.2.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 (231) hide show
  1. package/README.md +5 -19
  2. package/dist/blob-ref.d.ts +21 -0
  3. package/dist/blob-ref.js +97 -0
  4. package/dist/bytes.d.ts +45 -0
  5. package/dist/bytes.js +242 -0
  6. package/dist/canonical-json.d.ts +10 -0
  7. package/dist/canonical-json.js +18 -0
  8. package/dist/constants.d.ts +5 -0
  9. package/dist/constants.js +5 -0
  10. package/dist/crypto.d.ts +60 -0
  11. package/dist/crypto.js +251 -0
  12. package/dist/errors.d.ts +14 -0
  13. package/dist/errors.js +17 -0
  14. package/dist/frames.d.ts +24 -0
  15. package/dist/frames.js +42 -0
  16. package/dist/index.d.ts +18 -26
  17. package/dist/index.js +18 -43
  18. package/dist/message.d.ts +157 -0
  19. package/dist/message.js +786 -0
  20. package/dist/realtime.d.ts +92 -0
  21. package/dist/realtime.js +222 -0
  22. package/dist/render.d.ts +22 -0
  23. package/dist/render.js +273 -0
  24. package/dist/row-codec.d.ts +39 -0
  25. package/dist/row-codec.js +176 -0
  26. package/dist/segment.d.ts +18 -0
  27. package/dist/segment.js +105 -0
  28. package/dist/stream.d.ts +20 -0
  29. package/dist/stream.js +102 -0
  30. package/package.json +7 -59
  31. package/src/blob-ref.ts +135 -0
  32. package/src/bytes.ts +301 -0
  33. package/src/canonical-json.ts +20 -0
  34. package/src/constants.ts +7 -0
  35. package/src/crypto.ts +335 -0
  36. package/src/errors.ts +18 -0
  37. package/src/frames.ts +47 -0
  38. package/src/index.ts +18 -43
  39. package/src/message.ts +1024 -0
  40. package/src/realtime.ts +340 -0
  41. package/src/render.ts +325 -0
  42. package/src/row-codec.ts +265 -0
  43. package/src/segment.ts +150 -0
  44. package/src/stream.ts +128 -0
  45. package/dist/blobs.d.ts +0 -153
  46. package/dist/blobs.d.ts.map +0 -1
  47. package/dist/blobs.js +0 -47
  48. package/dist/blobs.js.map +0 -1
  49. package/dist/column-codecs.d.ts +0 -59
  50. package/dist/column-codecs.d.ts.map +0 -1
  51. package/dist/column-codecs.js +0 -159
  52. package/dist/column-codecs.js.map +0 -1
  53. package/dist/conflict.d.ts +0 -22
  54. package/dist/conflict.d.ts.map +0 -1
  55. package/dist/conflict.js +0 -81
  56. package/dist/conflict.js.map +0 -1
  57. package/dist/database.d.ts +0 -7
  58. package/dist/database.d.ts.map +0 -1
  59. package/dist/database.js +0 -14
  60. package/dist/database.js.map +0 -1
  61. package/dist/dialect.d.ts +0 -7
  62. package/dist/dialect.d.ts.map +0 -1
  63. package/dist/dialect.js +0 -2
  64. package/dist/dialect.js.map +0 -1
  65. package/dist/error-responses.d.ts +0 -393
  66. package/dist/error-responses.d.ts.map +0 -1
  67. package/dist/error-responses.js +0 -393
  68. package/dist/error-responses.js.map +0 -1
  69. package/dist/http/api-client.d.ts +0 -11
  70. package/dist/http/api-client.d.ts.map +0 -1
  71. package/dist/http/api-client.js +0 -30
  72. package/dist/http/api-client.js.map +0 -1
  73. package/dist/http/blob.d.ts +0 -6
  74. package/dist/http/blob.d.ts.map +0 -1
  75. package/dist/http/blob.js +0 -46
  76. package/dist/http/blob.js.map +0 -1
  77. package/dist/http/generated/api.d.ts +0 -5241
  78. package/dist/http/generated/api.d.ts.map +0 -1
  79. package/dist/http/generated/api.js +0 -6
  80. package/dist/http/generated/api.js.map +0 -1
  81. package/dist/http/index.d.ts +0 -18
  82. package/dist/http/index.d.ts.map +0 -1
  83. package/dist/http/index.js +0 -212
  84. package/dist/http/index.js.map +0 -1
  85. package/dist/http/shared.d.ts +0 -45
  86. package/dist/http/shared.d.ts.map +0 -1
  87. package/dist/http/shared.js +0 -122
  88. package/dist/http/shared.js.map +0 -1
  89. package/dist/http/transport-client.d.ts +0 -31
  90. package/dist/http/transport-client.d.ts.map +0 -1
  91. package/dist/http/transport-client.js +0 -193
  92. package/dist/http/transport-client.js.map +0 -1
  93. package/dist/index.d.ts.map +0 -1
  94. package/dist/index.js.map +0 -1
  95. package/dist/kysely-column-codecs.d.ts +0 -31
  96. package/dist/kysely-column-codecs.d.ts.map +0 -1
  97. package/dist/kysely-column-codecs.js +0 -452
  98. package/dist/kysely-column-codecs.js.map +0 -1
  99. package/dist/logger.d.ts +0 -29
  100. package/dist/logger.d.ts.map +0 -1
  101. package/dist/logger.js +0 -26
  102. package/dist/logger.js.map +0 -1
  103. package/dist/proxy/index.d.ts +0 -54
  104. package/dist/proxy/index.d.ts.map +0 -1
  105. package/dist/proxy/index.js +0 -7
  106. package/dist/proxy/index.js.map +0 -1
  107. package/dist/schemas/blobs.d.ts +0 -82
  108. package/dist/schemas/blobs.d.ts.map +0 -1
  109. package/dist/schemas/blobs.js +0 -64
  110. package/dist/schemas/blobs.js.map +0 -1
  111. package/dist/schemas/common.d.ts +0 -100
  112. package/dist/schemas/common.d.ts.map +0 -1
  113. package/dist/schemas/common.js +0 -64
  114. package/dist/schemas/common.js.map +0 -1
  115. package/dist/schemas/index.d.ts +0 -7
  116. package/dist/schemas/index.d.ts.map +0 -1
  117. package/dist/schemas/index.js +0 -7
  118. package/dist/schemas/index.js.map +0 -1
  119. package/dist/schemas/sync.d.ts +0 -1065
  120. package/dist/schemas/sync.d.ts.map +0 -1
  121. package/dist/schemas/sync.js +0 -361
  122. package/dist/schemas/sync.js.map +0 -1
  123. package/dist/scopes/index.d.ts +0 -113
  124. package/dist/scopes/index.d.ts.map +0 -1
  125. package/dist/scopes/index.js +0 -103
  126. package/dist/scopes/index.js.map +0 -1
  127. package/dist/sentry.d.ts +0 -31
  128. package/dist/sentry.d.ts.map +0 -1
  129. package/dist/sentry.js +0 -147
  130. package/dist/sentry.js.map +0 -1
  131. package/dist/snapshot-chunks.d.ts +0 -110
  132. package/dist/snapshot-chunks.d.ts.map +0 -1
  133. package/dist/snapshot-chunks.js +0 -682
  134. package/dist/snapshot-chunks.js.map +0 -1
  135. package/dist/sync-packs.d.ts +0 -33
  136. package/dist/sync-packs.d.ts.map +0 -1
  137. package/dist/sync-packs.js +0 -821
  138. package/dist/sync-packs.js.map +0 -1
  139. package/dist/telemetry.d.ts +0 -114
  140. package/dist/telemetry.d.ts.map +0 -1
  141. package/dist/telemetry.js +0 -114
  142. package/dist/telemetry.js.map +0 -1
  143. package/dist/transforms.d.ts +0 -146
  144. package/dist/transforms.d.ts.map +0 -1
  145. package/dist/transforms.js +0 -155
  146. package/dist/transforms.js.map +0 -1
  147. package/dist/types.d.ts +0 -210
  148. package/dist/types.d.ts.map +0 -1
  149. package/dist/types.js +0 -20
  150. package/dist/types.js.map +0 -1
  151. package/dist/utils/bytes.d.ts +0 -4
  152. package/dist/utils/bytes.d.ts.map +0 -1
  153. package/dist/utils/bytes.js +0 -66
  154. package/dist/utils/bytes.js.map +0 -1
  155. package/dist/utils/compression.d.ts +0 -22
  156. package/dist/utils/compression.d.ts.map +0 -1
  157. package/dist/utils/compression.js +0 -129
  158. package/dist/utils/compression.js.map +0 -1
  159. package/dist/utils/crypto.d.ts +0 -13
  160. package/dist/utils/crypto.d.ts.map +0 -1
  161. package/dist/utils/crypto.js +0 -107
  162. package/dist/utils/crypto.js.map +0 -1
  163. package/dist/utils/id.d.ts +0 -3
  164. package/dist/utils/id.d.ts.map +0 -1
  165. package/dist/utils/id.js +0 -25
  166. package/dist/utils/id.js.map +0 -1
  167. package/dist/utils/index.d.ts +0 -10
  168. package/dist/utils/index.d.ts.map +0 -1
  169. package/dist/utils/index.js +0 -10
  170. package/dist/utils/index.js.map +0 -1
  171. package/dist/utils/internal-runtime.d.ts +0 -16
  172. package/dist/utils/internal-runtime.d.ts.map +0 -1
  173. package/dist/utils/internal-runtime.js +0 -14
  174. package/dist/utils/internal-runtime.js.map +0 -1
  175. package/dist/utils/object.d.ts +0 -2
  176. package/dist/utils/object.d.ts.map +0 -1
  177. package/dist/utils/object.js +0 -4
  178. package/dist/utils/object.js.map +0 -1
  179. package/dist/utils/realtime-change-index.d.ts +0 -9
  180. package/dist/utils/realtime-change-index.d.ts.map +0 -1
  181. package/dist/utils/realtime-change-index.js +0 -36
  182. package/dist/utils/realtime-change-index.js.map +0 -1
  183. package/dist/utils/realtime-connection-registry.d.ts +0 -40
  184. package/dist/utils/realtime-connection-registry.d.ts.map +0 -1
  185. package/dist/utils/realtime-connection-registry.js +0 -237
  186. package/dist/utils/realtime-connection-registry.js.map +0 -1
  187. package/dist/utils/table-registry.d.ts +0 -10
  188. package/dist/utils/table-registry.d.ts.map +0 -1
  189. package/dist/utils/table-registry.js +0 -54
  190. package/dist/utils/table-registry.js.map +0 -1
  191. package/dist/utils/url.d.ts +0 -3
  192. package/dist/utils/url.d.ts.map +0 -1
  193. package/dist/utils/url.js +0 -20
  194. package/dist/utils/url.js.map +0 -1
  195. package/src/blobs.ts +0 -219
  196. package/src/column-codecs.ts +0 -262
  197. package/src/conflict.ts +0 -92
  198. package/src/database.ts +0 -21
  199. package/src/dialect.ts +0 -8
  200. package/src/error-responses.ts +0 -423
  201. package/src/http/api-client.ts +0 -39
  202. package/src/http/blob.ts +0 -95
  203. package/src/http/generated/api.ts +0 -5241
  204. package/src/http/index.ts +0 -365
  205. package/src/http/shared.ts +0 -220
  206. package/src/http/transport-client.ts +0 -314
  207. package/src/kysely-column-codecs.ts +0 -585
  208. package/src/logger.ts +0 -38
  209. package/src/proxy/index.ts +0 -57
  210. package/src/schemas/blobs.ts +0 -110
  211. package/src/schemas/common.ts +0 -89
  212. package/src/schemas/index.ts +0 -7
  213. package/src/schemas/sync.ts +0 -517
  214. package/src/scopes/index.ts +0 -237
  215. package/src/sentry.ts +0 -200
  216. package/src/snapshot-chunks.ts +0 -964
  217. package/src/sync-packs.ts +0 -1096
  218. package/src/telemetry.ts +0 -243
  219. package/src/transforms.ts +0 -256
  220. package/src/types.ts +0 -267
  221. package/src/utils/bytes.ts +0 -75
  222. package/src/utils/compression.ts +0 -187
  223. package/src/utils/crypto.ts +0 -146
  224. package/src/utils/id.ts +0 -34
  225. package/src/utils/index.ts +0 -9
  226. package/src/utils/internal-runtime.ts +0 -39
  227. package/src/utils/object.ts +0 -3
  228. package/src/utils/realtime-change-index.ts +0 -44
  229. package/src/utils/realtime-connection-registry.ts +0 -286
  230. package/src/utils/table-registry.ts +0 -67
  231. package/src/utils/url.ts +0 -29
@@ -0,0 +1,265 @@
1
+ /**
2
+ * Schema-IR row codec (SPEC.md §2.4).
3
+ *
4
+ * A row is a null bitmap of ceil(columnCount / 8) bytes (bit i set =
5
+ * column i is NULL; LSB-first within each byte, byte i/8), followed by the
6
+ * non-null values encoded positionally in schema-IR declaration order.
7
+ * Used for `COMMIT` change payloads, rows-segment row data, push operation
8
+ * payloads, and conflict `serverRow` values.
9
+ */
10
+ import { parseBlobRef } from './blob-ref';
11
+ import { ByteReader, ByteWriter } from './bytes';
12
+ import { DecodeError } from './errors';
13
+
14
+ export type ColumnType =
15
+ | 'string'
16
+ | 'integer'
17
+ | 'float'
18
+ | 'boolean'
19
+ | 'json'
20
+ | 'bytes'
21
+ | 'blob_ref'
22
+ | 'crdt';
23
+
24
+ export interface RowColumn {
25
+ readonly name: string;
26
+ readonly type: ColumnType;
27
+ readonly nullable: boolean;
28
+ /**
29
+ * For a `crdt` column (§2.4 tag 8, §5.10.1): the named merger the server
30
+ * selects (this rung defines exactly `'yjs-doc'`). Schema-IR metadata
31
+ * ONLY — never on the wire (the SSG2 column table carries name/type/
32
+ * nullable, and `crdt` shares the `bytes` tag). Ignored for other types.
33
+ */
34
+ readonly crdtType?: string;
35
+ /**
36
+ * §5.11: this column is encrypted end-to-end. When set, `type` is `bytes`
37
+ * (the wire/stored type — the ciphertext envelope rides the bytes machinery)
38
+ * and `declaredType` is the pre-flip app type. Both are schema-IR metadata,
39
+ * never on the wire (like `crdtType`). The row codec ignores these — it
40
+ * only ever sees a `bytes` value; encryption/decryption is the client
41
+ * encode/apply seam (§5.11).
42
+ */
43
+ readonly encrypted?: boolean;
44
+ /** §5.11: the app-side type of an encrypted column (its type before the
45
+ * wire flip to `bytes`). Present iff `encrypted` is set. */
46
+ readonly declaredType?: ColumnType;
47
+ }
48
+
49
+ /**
50
+ * `integer` is `i64` within the ±(2^53−1) contract; `json` is the raw JSON
51
+ * document string, preserved byte-for-byte on round-trip.
52
+ */
53
+ export type RowValue = string | number | boolean | Uint8Array | null;
54
+
55
+ /** Wire tags, unchanged from v1's binary-table-v1 assignment (SPEC.md §2.4). */
56
+ const TYPE_TO_TAG: Readonly<Record<ColumnType, number>> = {
57
+ string: 1,
58
+ integer: 2,
59
+ float: 3,
60
+ boolean: 4,
61
+ json: 5,
62
+ bytes: 6,
63
+ blob_ref: 7,
64
+ crdt: 8,
65
+ };
66
+
67
+ const TAG_TO_TYPE = new Map<number, ColumnType>(
68
+ Object.entries(TYPE_TO_TAG).map(([type, tag]) => [tag, type as ColumnType]),
69
+ );
70
+
71
+ export function columnTypeTag(type: ColumnType): number {
72
+ return TYPE_TO_TAG[type];
73
+ }
74
+
75
+ export function columnTypeFromTag(tag: number): ColumnType {
76
+ const type = TAG_TO_TYPE.get(tag);
77
+ if (type === undefined) {
78
+ throw new DecodeError(
79
+ 'sync.invalid_request',
80
+ `unknown column type tag ${tag}`,
81
+ );
82
+ }
83
+ return type;
84
+ }
85
+
86
+ function writeValue(
87
+ writer: ByteWriter,
88
+ column: RowColumn,
89
+ value: Exclude<RowValue, null>,
90
+ ): void {
91
+ switch (column.type) {
92
+ case 'string':
93
+ case 'json':
94
+ case 'blob_ref':
95
+ if (typeof value !== 'string') {
96
+ throw new Error(
97
+ `column ${column.name} (${column.type}) requires a string value`,
98
+ );
99
+ }
100
+ writer.str(value);
101
+ return;
102
+ case 'integer':
103
+ if (typeof value !== 'number' || !Number.isSafeInteger(value)) {
104
+ throw new Error(
105
+ `column ${column.name} (integer) requires a safe integer value`,
106
+ );
107
+ }
108
+ writer.i64(value);
109
+ return;
110
+ case 'float':
111
+ if (typeof value !== 'number') {
112
+ throw new Error(
113
+ `column ${column.name} (float) requires a number value`,
114
+ );
115
+ }
116
+ writer.f64(value);
117
+ return;
118
+ case 'boolean':
119
+ if (typeof value !== 'boolean') {
120
+ throw new Error(
121
+ `column ${column.name} (boolean) requires a boolean value`,
122
+ );
123
+ }
124
+ writer.bool(value);
125
+ return;
126
+ case 'bytes':
127
+ case 'crdt':
128
+ // §2.4 tag 8: a `crdt` value is byte-for-byte a `bytes` value —
129
+ // opaque CRDT bytes (§5.10), no structural validation. `crdtType`
130
+ // selects the server merger and never touches the codec.
131
+ if (!(value instanceof Uint8Array)) {
132
+ throw new Error(
133
+ `column ${column.name} (${column.type}) requires a Uint8Array value`,
134
+ );
135
+ }
136
+ writer.bytes(value);
137
+ return;
138
+ }
139
+ }
140
+
141
+ function readValue(reader: ByteReader, column: RowColumn): RowValue {
142
+ switch (column.type) {
143
+ case 'string':
144
+ return reader.str();
145
+ case 'json': {
146
+ // Conventions `json` MUST, applied at row-codec decode (SPEC.md §2.4
147
+ // tag 5): the value must parse as a JSON document; the raw string is
148
+ // preserved verbatim for round-trip fidelity.
149
+ const raw = reader.str();
150
+ try {
151
+ JSON.parse(raw);
152
+ } catch {
153
+ throw new DecodeError(
154
+ 'sync.invalid_request',
155
+ `json column ${column.name} does not parse as a JSON document`,
156
+ );
157
+ }
158
+ return raw;
159
+ }
160
+ case 'blob_ref': {
161
+ // §2.4 tag 7: the value is a canonical BlobRef JSON document
162
+ // (§5.9.1). Validated at decode, same class as tag-5 json; the raw
163
+ // string is preserved verbatim for re-encoding.
164
+ const raw = reader.str();
165
+ parseBlobRef(raw);
166
+ return raw;
167
+ }
168
+ case 'integer':
169
+ return reader.i64();
170
+ case 'float':
171
+ return reader.f64();
172
+ case 'boolean':
173
+ return reader.bool();
174
+ case 'bytes':
175
+ case 'crdt':
176
+ // §2.4 tag 8: opaque bytes, decoded exactly like tag 6 (no parse).
177
+ return reader.bytes();
178
+ }
179
+ }
180
+
181
+ export function writeRow(
182
+ writer: ByteWriter,
183
+ columns: readonly RowColumn[],
184
+ values: readonly RowValue[],
185
+ ): void {
186
+ if (values.length !== columns.length) {
187
+ throw new Error(
188
+ `row value count ${values.length} does not match column count ${columns.length}`,
189
+ );
190
+ }
191
+ const bitmapLength = Math.ceil(columns.length / 8);
192
+ const bitmap = new Uint8Array(bitmapLength);
193
+ for (let i = 0; i < columns.length; i++) {
194
+ const column = columns[i];
195
+ if (column === undefined) continue;
196
+ if (values[i] === null) {
197
+ if (!column.nullable) {
198
+ throw new Error(`column ${column.name} is not nullable`);
199
+ }
200
+ bitmap[i >> 3] = (bitmap[i >> 3] ?? 0) | (1 << (i & 7));
201
+ }
202
+ }
203
+ writer.raw(bitmap);
204
+ for (let i = 0; i < columns.length; i++) {
205
+ const column = columns[i];
206
+ const value = values[i];
207
+ if (column === undefined || value === null || value === undefined) continue;
208
+ writeValue(writer, column, value);
209
+ }
210
+ }
211
+
212
+ export function readRow(
213
+ reader: ByteReader,
214
+ columns: readonly RowColumn[],
215
+ ): RowValue[] {
216
+ const bitmapLength = Math.ceil(columns.length / 8);
217
+ const bitmap = reader.raw(bitmapLength);
218
+ for (let i = columns.length; i < bitmapLength * 8; i++) {
219
+ if ((((bitmap[i >> 3] ?? 0) >> (i & 7)) & 1) !== 0) {
220
+ throw new DecodeError(
221
+ 'sync.invalid_request',
222
+ 'non-zero padding bit in row null bitmap',
223
+ );
224
+ }
225
+ }
226
+ const values: RowValue[] = [];
227
+ for (let i = 0; i < columns.length; i++) {
228
+ const column = columns[i];
229
+ if (column === undefined) continue;
230
+ const isNull = (((bitmap[i >> 3] ?? 0) >> (i & 7)) & 1) !== 0;
231
+ if (isNull) {
232
+ if (!column.nullable) {
233
+ throw new DecodeError(
234
+ 'sync.invalid_request',
235
+ `null bit set for non-nullable column ${column.name}`,
236
+ );
237
+ }
238
+ values.push(null);
239
+ } else {
240
+ values.push(readValue(reader, column));
241
+ }
242
+ }
243
+ return values;
244
+ }
245
+
246
+ /** Encode one standalone row (e.g. a push payload or conflict serverRow). */
247
+ export function encodeRow(
248
+ columns: readonly RowColumn[],
249
+ values: readonly RowValue[],
250
+ ): Uint8Array {
251
+ const writer = new ByteWriter();
252
+ writeRow(writer, columns, values);
253
+ return writer.finish();
254
+ }
255
+
256
+ /** Decode one standalone row; the bytes must contain exactly one row. */
257
+ export function decodeRow(
258
+ columns: readonly RowColumn[],
259
+ bytes: Uint8Array,
260
+ ): RowValue[] {
261
+ const reader = new ByteReader(bytes);
262
+ const values = readRow(reader, columns);
263
+ reader.expectFullyConsumed('row payload');
264
+ return values;
265
+ }
package/src/segment.ts ADDED
@@ -0,0 +1,150 @@
1
+ /**
2
+ * SSG2 rows segments (SPEC.md §5.2) — the mandatory bootstrap format.
3
+ *
4
+ * A standalone binary container: magic, format version, flags, a column
5
+ * descriptor header (validation checksum, never inference), then
6
+ * self-delimiting row blocks terminated by a mandatory `rowCount = 0` end
7
+ * marker. Each row record carries the row's current `server_version`
8
+ * (`serverVersion`, ≥ 1) ahead of the row-codec bytes, so segment-applied
9
+ * rows participate in §6.2 conflict detection (§5.6).
10
+ */
11
+ import { ByteReader, ByteWriter, utf8Encode } from './bytes';
12
+ import { DecodeError } from './errors';
13
+ import {
14
+ columnTypeFromTag,
15
+ columnTypeTag,
16
+ type RowColumn,
17
+ type RowValue,
18
+ readRow,
19
+ writeRow,
20
+ } from './row-codec';
21
+
22
+ export const ROWS_SEGMENT_MAGIC = 'SSG2';
23
+ export const ROWS_SEGMENT_FORMAT_VERSION = 1;
24
+
25
+ const MAGIC_BYTES = utf8Encode(ROWS_SEGMENT_MAGIC);
26
+
27
+ /** One §5.2 row record: the row's `server_version` plus its values. */
28
+ export interface SegmentRow {
29
+ /** The row's `server_version` at `asOfCommitSeq` (§2.2); always ≥ 1. */
30
+ readonly serverVersion: number;
31
+ readonly values: readonly RowValue[];
32
+ }
33
+
34
+ export interface RowsSegment {
35
+ readonly table: string;
36
+ readonly schemaVersion: number;
37
+ readonly columns: readonly RowColumn[];
38
+ /** Row blocks in wire order; each block is applied in one transaction. */
39
+ readonly blocks: readonly (readonly SegmentRow[])[];
40
+ }
41
+
42
+ export function encodeRowsSegment(segment: RowsSegment): Uint8Array {
43
+ const writer = new ByteWriter();
44
+ writer.raw(MAGIC_BYTES);
45
+ writer.u16(ROWS_SEGMENT_FORMAT_VERSION);
46
+ writer.u16(0);
47
+ writer.str(segment.table);
48
+ writer.i32(segment.schemaVersion);
49
+ writer.u16(segment.columns.length);
50
+ for (const column of segment.columns) {
51
+ writer.str(column.name);
52
+ writer.u8(columnTypeTag(column.type));
53
+ writer.u8(column.nullable ? 1 : 0);
54
+ }
55
+ for (const block of segment.blocks) {
56
+ if (block.length === 0) {
57
+ throw new Error('a rows-segment block must contain at least one row');
58
+ }
59
+ const rowsWriter = new ByteWriter();
60
+ for (const row of block) {
61
+ if (row.serverVersion < 1) {
62
+ throw new Error(
63
+ `a rows-segment row serverVersion must be >= 1, got ${row.serverVersion}`,
64
+ );
65
+ }
66
+ rowsWriter.i64(row.serverVersion);
67
+ writeRow(rowsWriter, segment.columns, row.values);
68
+ }
69
+ const rows = rowsWriter.finish();
70
+ writer.u32(block.length);
71
+ writer.u32(rows.length);
72
+ writer.raw(rows);
73
+ }
74
+ writer.u32(0);
75
+ return writer.finish();
76
+ }
77
+
78
+ export function decodeRowsSegment(bytes: Uint8Array): RowsSegment {
79
+ const reader = new ByteReader(bytes);
80
+ const magic = reader.raw(4);
81
+ if (
82
+ magic[0] !== MAGIC_BYTES[0] ||
83
+ magic[1] !== MAGIC_BYTES[1] ||
84
+ magic[2] !== MAGIC_BYTES[2] ||
85
+ magic[3] !== MAGIC_BYTES[3]
86
+ ) {
87
+ throw new DecodeError('sync.invalid_request', 'bad rows segment magic');
88
+ }
89
+ const formatVersion = reader.u16();
90
+ if (formatVersion !== ROWS_SEGMENT_FORMAT_VERSION) {
91
+ throw new DecodeError(
92
+ 'sync.invalid_request',
93
+ `unsupported rows segment format version ${formatVersion}`,
94
+ );
95
+ }
96
+ const flags = reader.u16();
97
+ if (flags !== 0) {
98
+ throw new DecodeError(
99
+ 'sync.invalid_request',
100
+ `non-zero rows segment flags 0x${flags.toString(16)}`,
101
+ );
102
+ }
103
+ const table = reader.str();
104
+ const schemaVersion = reader.i32();
105
+ const columnCount = reader.u16();
106
+ const columns: RowColumn[] = [];
107
+ for (let i = 0; i < columnCount; i++) {
108
+ const name = reader.str();
109
+ const type = columnTypeFromTag(reader.u8());
110
+ const columnFlags = reader.u8();
111
+ if ((columnFlags & 0xfe) !== 0) {
112
+ throw new DecodeError(
113
+ 'sync.invalid_request',
114
+ `reserved column flag bits set for column ${name}`,
115
+ );
116
+ }
117
+ columns.push({ name, type, nullable: columnFlags === 1 });
118
+ }
119
+ const blocks: SegmentRow[][] = [];
120
+ for (;;) {
121
+ if (reader.remaining < 4) {
122
+ throw new DecodeError(
123
+ 'sync.invalid_request',
124
+ 'rows segment truncated: missing end marker',
125
+ );
126
+ }
127
+ const rowCount = reader.u32();
128
+ if (rowCount === 0) {
129
+ reader.expectFullyConsumed('rows segment end marker');
130
+ break;
131
+ }
132
+ const byteLength = reader.u32();
133
+ const rowBytes = reader.raw(byteLength);
134
+ const blockReader = new ByteReader(rowBytes);
135
+ const block: SegmentRow[] = [];
136
+ for (let i = 0; i < rowCount; i++) {
137
+ const serverVersion = blockReader.i64();
138
+ if (serverVersion < 1) {
139
+ throw new DecodeError(
140
+ 'sync.invalid_request',
141
+ `rows segment row serverVersion must be >= 1, got ${serverVersion}`,
142
+ );
143
+ }
144
+ block.push({ serverVersion, values: readRow(blockReader, columns) });
145
+ }
146
+ blockReader.expectFullyConsumed('rows block');
147
+ blocks.push(block);
148
+ }
149
+ return { table, schemaVersion, columns, blocks };
150
+ }
package/src/stream.ts ADDED
@@ -0,0 +1,128 @@
1
+ /**
2
+ * Incremental SSP2 message-stream scanner (SPEC.md §1.4, §8.7).
3
+ *
4
+ * The envelope grammar is self-delimiting — an 8-byte header, then
5
+ * length-prefixed frames until `END` — so a byte stream split across
6
+ * arbitrary chunk boundaries (WebSocket messages, §8.7) needs no
7
+ * reassembly protocol beyond concatenation plus this scanner: feed
8
+ * chunks, learn exactly where one complete envelope ends. Used by the
9
+ * server session (request assembly, pipelining detection), the client
10
+ * (response assembly), and the conformance harness (round attribution).
11
+ *
12
+ * The scanner validates only the 8-byte header (a stream whose header is
13
+ * not a valid SSP2 envelope has no findable end, §8.7 connection-fatal
14
+ * rule) and walks frame length prefixes; full decoding stays with
15
+ * `decodeMessage`.
16
+ */
17
+ import { utf8Encode } from './bytes';
18
+ import { PROTOCOL_WIRE_VERSION, SYNC_PACK_MAGIC } from './constants';
19
+ import { DecodeError } from './errors';
20
+
21
+ const MAGIC = utf8Encode(SYNC_PACK_MAGIC);
22
+ /** `END` frame type byte (§1.2). */
23
+ const END_FRAME_TYPE = 0x00;
24
+
25
+ export interface ScannedMessage {
26
+ /** The complete envelope bytes (header through the END frame). */
27
+ readonly message: Uint8Array;
28
+ /** Byte count buffered PAST the END frame — a §8.7 stream MUST end
29
+ * exactly at the END frame's last byte, so any excess is a protocol
30
+ * violation (pipelining) for the caller to act on. */
31
+ readonly excess: number;
32
+ }
33
+
34
+ export class MessageStreamScanner {
35
+ #buffer = new Uint8Array(1024);
36
+ #length = 0;
37
+ /** Parse cursor: start of the next frame header; -1 while the 8-byte
38
+ * envelope header is still incomplete. */
39
+ #offset = -1;
40
+ #complete = false;
41
+
42
+ /** True once any bytes were fed. */
43
+ get started(): boolean {
44
+ return this.#length > 0;
45
+ }
46
+
47
+ /**
48
+ * Feed one chunk. Returns the completed message once the END frame is
49
+ * fully buffered, `undefined` while more bytes are needed. Throws
50
+ * `DecodeError` on an invalid envelope header (connection-fatal per
51
+ * §8.7) and plain `Error` on use after completion.
52
+ */
53
+ push(chunk: Uint8Array): ScannedMessage | undefined {
54
+ if (this.#complete) {
55
+ throw new Error('MessageStreamScanner: message already complete');
56
+ }
57
+ this.#append(chunk);
58
+ if (this.#offset < 0) {
59
+ if (this.#length < 8) return undefined;
60
+ this.#checkHeader();
61
+ this.#offset = 8;
62
+ }
63
+ for (;;) {
64
+ if (this.#length - this.#offset < 5) return undefined;
65
+ const frameType = this.#buffer[this.#offset] as number;
66
+ const view = new DataView(
67
+ this.#buffer.buffer,
68
+ this.#buffer.byteOffset + this.#offset + 1,
69
+ 4,
70
+ );
71
+ const frameLength = view.getUint32(0, true);
72
+ const frameEnd = this.#offset + 5 + frameLength;
73
+ if (this.#length < frameEnd) return undefined;
74
+ this.#offset = frameEnd;
75
+ if (frameType === END_FRAME_TYPE) {
76
+ this.#complete = true;
77
+ return {
78
+ message: this.#buffer.subarray(0, frameEnd),
79
+ excess: this.#length - frameEnd,
80
+ };
81
+ }
82
+ }
83
+ }
84
+
85
+ #append(chunk: Uint8Array): void {
86
+ const needed = this.#length + chunk.length;
87
+ if (needed > this.#buffer.length) {
88
+ let capacity = this.#buffer.length;
89
+ while (capacity < needed) capacity *= 2;
90
+ const grown = new Uint8Array(capacity);
91
+ grown.set(this.#buffer.subarray(0, this.#length));
92
+ this.#buffer = grown;
93
+ }
94
+ this.#buffer.set(chunk, this.#length);
95
+ this.#length = needed;
96
+ }
97
+
98
+ #checkHeader(): void {
99
+ const b = this.#buffer;
100
+ if (
101
+ b[0] !== MAGIC[0] ||
102
+ b[1] !== MAGIC[1] ||
103
+ b[2] !== MAGIC[2] ||
104
+ b[3] !== MAGIC[3]
105
+ ) {
106
+ throw new DecodeError('sync.invalid_request', 'bad envelope magic');
107
+ }
108
+ const wireVersion = (b[4] as number) | ((b[5] as number) << 8);
109
+ if (wireVersion !== PROTOCOL_WIRE_VERSION) {
110
+ throw new DecodeError(
111
+ 'sync.invalid_request',
112
+ `unsupported wireVersion ${wireVersion}`,
113
+ );
114
+ }
115
+ if (b[6] !== 0x01 && b[6] !== 0x02) {
116
+ throw new DecodeError(
117
+ 'sync.invalid_request',
118
+ `unknown msgKind byte 0x${(b[6] as number).toString(16)}`,
119
+ );
120
+ }
121
+ if (b[7] !== 0x00) {
122
+ throw new DecodeError(
123
+ 'sync.invalid_request',
124
+ `non-zero envelope flags 0x${(b[7] as number).toString(16)}`,
125
+ );
126
+ }
127
+ }
128
+ }
package/dist/blobs.d.ts DELETED
@@ -1,153 +0,0 @@
1
- /**
2
- * @syncular/core - Blob types for media/binary handling
3
- *
4
- * Content-addressable blob storage with presigned URL support.
5
- * Protocol types (BlobRef, BlobMetadata, etc.) live in ./schemas/blobs.ts
6
- */
7
- import type { BlobRef } from './schemas/blobs';
8
- /**
9
- * Transport interface for client-server blob communication.
10
- * This is used by the client blob manager to communicate with the server.
11
- */
12
- export interface BlobTransport {
13
- /**
14
- * Initiate a blob upload.
15
- * Returns presigned URL info or indicates blob already exists (dedup).
16
- */
17
- initiateUpload(args: {
18
- hash: string;
19
- size: number;
20
- mimeType: string;
21
- }): Promise<{
22
- exists: boolean;
23
- uploadUrl?: string;
24
- uploadMethod?: 'PUT' | 'POST';
25
- uploadHeaders?: Record<string, string>;
26
- }>;
27
- /**
28
- * Complete a blob upload.
29
- * Call this after uploading to the presigned URL.
30
- */
31
- completeUpload(hash: string): Promise<{
32
- ok: boolean;
33
- error?: string;
34
- }>;
35
- /**
36
- * Get a presigned download URL.
37
- */
38
- getDownloadUrl(hash: string): Promise<{
39
- url: string;
40
- expiresAt: string;
41
- }>;
42
- }
43
- /**
44
- * Options for signing an upload URL.
45
- */
46
- export interface BlobSignUploadOptions {
47
- /** SHA-256 hash (for naming and checksum validation) */
48
- hash: string;
49
- /** Optional tenant/partition namespace for storage lookups */
50
- partitionId?: string;
51
- /** Content size in bytes */
52
- size: number;
53
- /** MIME type */
54
- mimeType: string;
55
- /** URL expiration in seconds */
56
- expiresIn: number;
57
- }
58
- /**
59
- * Result of signing an upload URL.
60
- */
61
- export interface BlobSignedUpload {
62
- /** The URL to upload to */
63
- url: string;
64
- /** HTTP method */
65
- method: 'PUT' | 'POST';
66
- /** Required headers */
67
- headers?: Record<string, string>;
68
- }
69
- /**
70
- * Options for signing a download URL.
71
- */
72
- export interface BlobSignDownloadOptions {
73
- /** SHA-256 hash */
74
- hash: string;
75
- /** Optional tenant/partition namespace for storage lookups */
76
- partitionId?: string;
77
- /** URL expiration in seconds */
78
- expiresIn: number;
79
- }
80
- export interface BlobStoragePartitionOptions {
81
- partitionId?: string;
82
- }
83
- /**
84
- * Adapter for blob storage backends (S3, R2, custom).
85
- * Implementations handle actual storage; the sync server orchestrates.
86
- */
87
- export interface BlobStorageAdapter {
88
- /** Adapter name for logging/debugging */
89
- readonly name: string;
90
- /**
91
- * Generate a presigned URL for uploading a blob.
92
- * The URL should enforce checksum validation if the backend supports it.
93
- */
94
- signUpload(options: BlobSignUploadOptions): Promise<BlobSignedUpload>;
95
- /**
96
- * Generate a presigned URL for downloading a blob.
97
- */
98
- signDownload(options: BlobSignDownloadOptions): Promise<string>;
99
- /**
100
- * Check if a blob exists in storage.
101
- */
102
- exists(hash: string, options?: BlobStoragePartitionOptions): Promise<boolean>;
103
- /**
104
- * Delete a blob (for garbage collection).
105
- */
106
- delete(hash: string, options?: BlobStoragePartitionOptions): Promise<void>;
107
- /**
108
- * Get blob metadata from storage (optional).
109
- * Used to verify uploads completed successfully.
110
- */
111
- getMetadata?(hash: string, options?: BlobStoragePartitionOptions): Promise<{
112
- size: number;
113
- mimeType?: string;
114
- } | null>;
115
- /**
116
- * Store blob data directly (for adapters that support direct storage).
117
- * Used for snapshot chunks and other internal data.
118
- */
119
- put?(hash: string, data: Uint8Array, metadata?: Record<string, unknown>, options?: BlobStoragePartitionOptions): Promise<void>;
120
- /**
121
- * Store blob data directly from a stream.
122
- * Preferred for large payloads to avoid full buffering in memory.
123
- */
124
- putStream?(hash: string, stream: ReadableStream<Uint8Array>, metadata?: Record<string, unknown>, options?: BlobStoragePartitionOptions): Promise<void>;
125
- /**
126
- * Get blob data directly (for adapters that support direct retrieval).
127
- */
128
- get?(hash: string, options?: BlobStoragePartitionOptions): Promise<Uint8Array | null>;
129
- /**
130
- * Get blob data directly as a stream (for adapters that support stream retrieval).
131
- */
132
- getStream?(hash: string, options?: BlobStoragePartitionOptions): Promise<ReadableStream<Uint8Array> | null>;
133
- }
134
- /**
135
- * Create a BlobRef from upload metadata.
136
- */
137
- export declare function createBlobRef(args: {
138
- hash: string;
139
- size: number;
140
- mimeType: string;
141
- encrypted?: boolean;
142
- keyId?: string;
143
- }): BlobRef;
144
- /**
145
- * Parse a blob hash, validating format.
146
- * @returns The hex hash without prefix, or null if invalid.
147
- */
148
- export declare function parseBlobHash(hash: string): string | null;
149
- /**
150
- * Create a blob hash string from hex.
151
- */
152
- export declare function createBlobHash(hexHash: string): string;
153
- //# sourceMappingURL=blobs.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"blobs.d.ts","sourceRoot":"","sources":["../src/blobs.ts"],"names":[],"mappings":"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"}