@prisma/streams-server 0.0.1 → 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 (83) hide show
  1. package/CODE_OF_CONDUCT.md +45 -0
  2. package/CONTRIBUTING.md +68 -0
  3. package/LICENSE +201 -0
  4. package/README.md +39 -2
  5. package/SECURITY.md +33 -0
  6. package/bin/prisma-streams-server +2 -0
  7. package/package.json +29 -34
  8. package/src/app.ts +74 -0
  9. package/src/app_core.ts +1706 -0
  10. package/src/app_local.ts +46 -0
  11. package/src/backpressure.ts +66 -0
  12. package/src/bootstrap.ts +239 -0
  13. package/src/config.ts +251 -0
  14. package/src/db/db.ts +1386 -0
  15. package/src/db/schema.ts +625 -0
  16. package/src/expiry_sweeper.ts +44 -0
  17. package/src/hist.ts +169 -0
  18. package/src/index/binary_fuse.ts +379 -0
  19. package/src/index/indexer.ts +745 -0
  20. package/src/index/run_cache.ts +84 -0
  21. package/src/index/run_format.ts +213 -0
  22. package/src/ingest.ts +655 -0
  23. package/src/lens/lens.ts +501 -0
  24. package/src/manifest.ts +114 -0
  25. package/src/memory.ts +155 -0
  26. package/src/metrics.ts +161 -0
  27. package/src/metrics_emitter.ts +50 -0
  28. package/src/notifier.ts +64 -0
  29. package/src/objectstore/interface.ts +13 -0
  30. package/src/objectstore/mock_r2.ts +269 -0
  31. package/src/objectstore/null.ts +32 -0
  32. package/src/objectstore/r2.ts +128 -0
  33. package/src/offset.ts +70 -0
  34. package/src/reader.ts +454 -0
  35. package/src/runtime/hash.ts +156 -0
  36. package/src/runtime/hash_vendor/LICENSE.hash-wasm +38 -0
  37. package/src/runtime/hash_vendor/NOTICE.md +8 -0
  38. package/src/runtime/hash_vendor/xxhash3.umd.min.cjs +7 -0
  39. package/src/runtime/hash_vendor/xxhash32.umd.min.cjs +7 -0
  40. package/src/runtime/hash_vendor/xxhash64.umd.min.cjs +7 -0
  41. package/src/schema/lens_schema.ts +290 -0
  42. package/src/schema/proof.ts +547 -0
  43. package/src/schema/registry.ts +405 -0
  44. package/src/segment/cache.ts +179 -0
  45. package/src/segment/format.ts +331 -0
  46. package/src/segment/segmenter.ts +326 -0
  47. package/src/segment/segmenter_worker.ts +43 -0
  48. package/src/segment/segmenter_workers.ts +94 -0
  49. package/src/server.ts +326 -0
  50. package/src/sqlite/adapter.ts +164 -0
  51. package/src/stats.ts +205 -0
  52. package/src/touch/engine.ts +41 -0
  53. package/src/touch/interpreter_worker.ts +442 -0
  54. package/src/touch/live_keys.ts +118 -0
  55. package/src/touch/live_metrics.ts +827 -0
  56. package/src/touch/live_templates.ts +619 -0
  57. package/src/touch/manager.ts +1199 -0
  58. package/src/touch/spec.ts +456 -0
  59. package/src/touch/touch_journal.ts +671 -0
  60. package/src/touch/touch_key_id.ts +20 -0
  61. package/src/touch/worker_pool.ts +189 -0
  62. package/src/touch/worker_protocol.ts +56 -0
  63. package/src/types/proper-lockfile.d.ts +1 -0
  64. package/src/uploader.ts +317 -0
  65. package/src/util/base32_crockford.ts +81 -0
  66. package/src/util/bloom256.ts +67 -0
  67. package/src/util/cleanup.ts +22 -0
  68. package/src/util/crc32c.ts +29 -0
  69. package/src/util/ds_error.ts +15 -0
  70. package/src/util/duration.ts +17 -0
  71. package/src/util/endian.ts +53 -0
  72. package/src/util/json_pointer.ts +148 -0
  73. package/src/util/log.ts +25 -0
  74. package/src/util/lru.ts +45 -0
  75. package/src/util/retry.ts +35 -0
  76. package/src/util/siphash.ts +71 -0
  77. package/src/util/stream_paths.ts +31 -0
  78. package/src/util/time.ts +14 -0
  79. package/src/util/yield.ts +3 -0
  80. package/build/index.d.mts +0 -1
  81. package/build/index.d.ts +0 -1
  82. package/build/index.js +0 -0
  83. package/build/index.mjs +0 -1
@@ -0,0 +1,269 @@
1
+ import { createHash } from "node:crypto";
2
+ import { mkdirSync, readFileSync, writeFileSync, unlinkSync, openSync, closeSync, readSync, copyFileSync, createReadStream } from "node:fs";
3
+ import { dirname, join } from "node:path";
4
+ import type { GetOptions, ObjectStore, PutResult } from "./interface";
5
+ import { dsError } from "../util/ds_error.ts";
6
+
7
+ export type MockR2Faults = {
8
+ putDelayMs?: number;
9
+ getDelayMs?: number;
10
+ headDelayMs?: number;
11
+ listDelayMs?: number;
12
+ failPutPrefix?: string; // fail PUT when key starts with prefix
13
+ failPutEvery?: number; // fail every Nth PUT
14
+ failGetEvery?: number; // fail every Nth GET
15
+ failHeadEvery?: number;
16
+ failListEvery?: number;
17
+ timeoutPutEvery?: number;
18
+ timeoutGetEvery?: number;
19
+ timeoutHeadEvery?: number;
20
+ timeoutListEvery?: number;
21
+ partialGetEvery?: number; // return truncated bytes every Nth GET
22
+ };
23
+
24
+ export type MockR2Options = {
25
+ faults?: MockR2Faults;
26
+ maxInMemoryBytes?: number; // spill when object exceeds this
27
+ spillDir?: string; // optional directory for large objects
28
+ };
29
+
30
+ type StoredObject = {
31
+ etag: string;
32
+ size: number;
33
+ bytes?: Uint8Array;
34
+ path?: string;
35
+ };
36
+
37
+ function sleep(ms: number): Promise<void> {
38
+ if (ms <= 0) return Promise.resolve();
39
+ return new Promise((res) => setTimeout(res, ms));
40
+ }
41
+
42
+ export class MockR2Store implements ObjectStore {
43
+ private readonly data = new Map<string, StoredObject>();
44
+ private readonly faults: MockR2Faults;
45
+ private readonly maxInMemoryBytes: number;
46
+ private readonly spillDir?: string;
47
+
48
+ private putCount = 0;
49
+ private getCount = 0;
50
+ private headCount = 0;
51
+ private listCount = 0;
52
+ private memBytes = 0;
53
+
54
+ constructor(opts: MockR2Options | MockR2Faults = {}) {
55
+ if ("failPutEvery" in opts || "putDelayMs" in opts) {
56
+ this.faults = opts as MockR2Faults;
57
+ this.maxInMemoryBytes = Number.POSITIVE_INFINITY;
58
+ } else {
59
+ const o = opts as MockR2Options;
60
+ this.faults = o.faults ?? {};
61
+ this.maxInMemoryBytes = o.maxInMemoryBytes ?? Number.POSITIVE_INFINITY;
62
+ this.spillDir = o.spillDir;
63
+ }
64
+ }
65
+
66
+ private mkEtag(bytes: Uint8Array): string {
67
+ return createHash("sha256").update(bytes).digest("hex");
68
+ }
69
+
70
+ private async hashFile(path: string): Promise<string> {
71
+ const hash = createHash("sha256");
72
+ await new Promise<void>((resolve, reject) => {
73
+ const stream = createReadStream(path);
74
+ stream.on("data", (chunk) => hash.update(chunk));
75
+ stream.on("error", (err) => reject(err));
76
+ stream.on("end", () => resolve());
77
+ });
78
+ return hash.digest("hex");
79
+ }
80
+
81
+ private mkPath(key: string): string {
82
+ const hex = createHash("sha256").update(key).digest("hex");
83
+ return this.spillDir ? join(this.spillDir, `${hex}.bin`) : hex;
84
+ }
85
+
86
+ private shouldSpill(len: number): boolean {
87
+ return this.spillDir != null && len > this.maxInMemoryBytes;
88
+ }
89
+
90
+ private maybeFail(count: number, every?: number, msg?: string): void {
91
+ if (every && count % every === 0) throw dsError(msg ?? "MockR2: injected failure");
92
+ }
93
+
94
+ private maybeTimeout(count: number, every?: number, msg?: string): void {
95
+ if (every && count % every === 0) throw dsError(msg ?? "MockR2: injected timeout");
96
+ }
97
+
98
+ async put(key: string, bytes: Uint8Array): Promise<PutResult> {
99
+ this.putCount++;
100
+ if (this.faults.failPutPrefix && key.startsWith(this.faults.failPutPrefix)) {
101
+ throw dsError(`MockR2: injected PUT failure for ${key}`);
102
+ }
103
+ this.maybeFail(this.putCount, this.faults.failPutEvery, `MockR2: injected PUT failure for ${key}`);
104
+ this.maybeTimeout(this.putCount, this.faults.timeoutPutEvery, `MockR2: injected PUT timeout for ${key}`);
105
+ await sleep(this.faults.putDelayMs ?? 0);
106
+
107
+ const copy = new Uint8Array(bytes);
108
+ const etag = this.mkEtag(copy);
109
+ const size = copy.byteLength;
110
+
111
+ const existing = this.data.get(key);
112
+ if (existing?.bytes) this.memBytes -= existing.bytes.byteLength;
113
+ if (existing?.path) {
114
+ try { unlinkSync(existing.path); } catch { /* ignore */ }
115
+ }
116
+
117
+ if (this.shouldSpill(size)) {
118
+ const path = this.mkPath(key);
119
+ mkdirSync(dirname(path), { recursive: true });
120
+ writeFileSync(path, copy);
121
+ this.data.set(key, { etag, size, path });
122
+ } else {
123
+ this.memBytes += size;
124
+ this.data.set(key, { etag, size, bytes: copy });
125
+ }
126
+
127
+ return { etag };
128
+ }
129
+
130
+ async putFile(key: string, path: string, size: number): Promise<PutResult> {
131
+ this.putCount++;
132
+ if (this.faults.failPutPrefix && key.startsWith(this.faults.failPutPrefix)) {
133
+ throw dsError(`MockR2: injected PUT failure for ${key}`);
134
+ }
135
+ this.maybeFail(this.putCount, this.faults.failPutEvery, `MockR2: injected PUT failure for ${key}`);
136
+ this.maybeTimeout(this.putCount, this.faults.timeoutPutEvery, `MockR2: injected PUT timeout for ${key}`);
137
+ await sleep(this.faults.putDelayMs ?? 0);
138
+
139
+ const etag = await this.hashFile(path);
140
+
141
+ const existing = this.data.get(key);
142
+ if (existing?.bytes) this.memBytes -= existing.bytes.byteLength;
143
+ if (existing?.path) {
144
+ try { unlinkSync(existing.path); } catch { /* ignore */ }
145
+ }
146
+
147
+ if (this.shouldSpill(size)) {
148
+ const dest = this.mkPath(key);
149
+ mkdirSync(dirname(dest), { recursive: true });
150
+ copyFileSync(path, dest);
151
+ this.data.set(key, { etag, size, path: dest });
152
+ } else {
153
+ const bytes = new Uint8Array(await Bun.file(path).arrayBuffer());
154
+ this.memBytes += bytes.byteLength;
155
+ this.data.set(key, { etag, size, bytes });
156
+ }
157
+
158
+ return { etag };
159
+ }
160
+
161
+ async get(key: string, opts: GetOptions = {}): Promise<Uint8Array | null> {
162
+ this.getCount++;
163
+ this.maybeFail(this.getCount, this.faults.failGetEvery, `MockR2: injected GET failure for ${key}`);
164
+ this.maybeTimeout(this.getCount, this.faults.timeoutGetEvery, `MockR2: injected GET timeout for ${key}`);
165
+ await sleep(this.faults.getDelayMs ?? 0);
166
+
167
+ const entry = this.data.get(key);
168
+ if (!entry) return null;
169
+
170
+ const range = opts.range;
171
+ const total = entry.size;
172
+ const start = range?.start ?? 0;
173
+ const end = range?.end ?? total - 1;
174
+ if (start >= total) return new Uint8Array(0);
175
+ const clampEnd = Math.min(end, total - 1);
176
+ const length = Math.max(0, clampEnd - start + 1);
177
+
178
+ let out: Uint8Array;
179
+ if (entry.bytes) {
180
+ out = entry.bytes.slice(start, start + length);
181
+ } else if (entry.path) {
182
+ if (length === total) {
183
+ out = new Uint8Array(readFileSync(entry.path));
184
+ } else {
185
+ const fd = openSync(entry.path, "r");
186
+ try {
187
+ const buf = new Uint8Array(length);
188
+ readSync(fd, buf, 0, length, start);
189
+ out = buf;
190
+ } finally {
191
+ closeSync(fd);
192
+ }
193
+ }
194
+ } else {
195
+ return null;
196
+ }
197
+
198
+ if (this.faults.partialGetEvery && this.getCount % this.faults.partialGetEvery === 0) {
199
+ const half = Math.max(0, Math.floor(out.byteLength / 2));
200
+ return out.slice(0, half);
201
+ }
202
+
203
+ return out;
204
+ }
205
+
206
+ async head(key: string): Promise<{ etag: string; size: number } | null> {
207
+ this.headCount++;
208
+ this.maybeFail(this.headCount, this.faults.failHeadEvery, `MockR2: injected HEAD failure for ${key}`);
209
+ this.maybeTimeout(this.headCount, this.faults.timeoutHeadEvery, `MockR2: injected HEAD timeout for ${key}`);
210
+ await sleep(this.faults.headDelayMs ?? 0);
211
+
212
+ const v = this.data.get(key);
213
+ if (!v) return null;
214
+ return { etag: v.etag, size: v.size };
215
+ }
216
+
217
+ async delete(key: string): Promise<void> {
218
+ const v = this.data.get(key);
219
+ if (v?.bytes) this.memBytes -= v.bytes.byteLength;
220
+ if (v?.path) {
221
+ try { unlinkSync(v.path); } catch { /* ignore */ }
222
+ }
223
+ this.data.delete(key);
224
+ }
225
+
226
+ async list(prefix: string): Promise<string[]> {
227
+ this.listCount++;
228
+ this.maybeFail(this.listCount, this.faults.failListEvery, "MockR2: injected LIST failure");
229
+ this.maybeTimeout(this.listCount, this.faults.timeoutListEvery, "MockR2: injected LIST timeout");
230
+ await sleep(this.faults.listDelayMs ?? 0);
231
+
232
+ const out: string[] = [];
233
+ for (const k of this.data.keys()) {
234
+ if (k.startsWith(prefix)) out.push(k);
235
+ }
236
+ out.sort();
237
+ return out;
238
+ }
239
+
240
+ // Helpers for tests
241
+ has(key: string): boolean {
242
+ return this.data.has(key);
243
+ }
244
+
245
+ size(): number {
246
+ return this.data.size;
247
+ }
248
+
249
+ memoryBytes(): number {
250
+ return this.memBytes;
251
+ }
252
+
253
+ stats(): { puts: number; gets: number; heads: number; lists: number; memoryBytes: number } {
254
+ return {
255
+ puts: this.putCount,
256
+ gets: this.getCount,
257
+ heads: this.headCount,
258
+ lists: this.listCount,
259
+ memoryBytes: this.memBytes,
260
+ };
261
+ }
262
+
263
+ resetStats(): void {
264
+ this.putCount = 0;
265
+ this.getCount = 0;
266
+ this.headCount = 0;
267
+ this.listCount = 0;
268
+ }
269
+ }
@@ -0,0 +1,32 @@
1
+ import type { GetOptions, ObjectStore, PutResult } from "./interface";
2
+ import { dsError } from "../util/ds_error.ts";
3
+
4
+ function disabled(op: string, key?: string): never {
5
+ throw dsError(`object store disabled in local mode (${op}${key ? `: ${key}` : ""})`);
6
+ }
7
+
8
+ export class NullObjectStore implements ObjectStore {
9
+ async put(key: string, _data: Uint8Array, _opts?: { contentType?: string; contentLength?: number }): Promise<PutResult> {
10
+ return disabled("put", key);
11
+ }
12
+
13
+ async putFile(key: string, _path: string, _size: number, _opts?: { contentType?: string }): Promise<PutResult> {
14
+ return disabled("putFile", key);
15
+ }
16
+
17
+ async get(key: string, _opts?: GetOptions): Promise<Uint8Array | null> {
18
+ return disabled("get", key);
19
+ }
20
+
21
+ async head(key: string): Promise<{ etag: string; size: number } | null> {
22
+ return disabled("head", key);
23
+ }
24
+
25
+ async delete(key: string): Promise<void> {
26
+ return disabled("delete", key);
27
+ }
28
+
29
+ async list(prefix: string): Promise<string[]> {
30
+ return disabled("list", prefix);
31
+ }
32
+ }
@@ -0,0 +1,128 @@
1
+ import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
3
+ import type { GetOptions, ObjectStore, PutResult } from "./interface";
4
+ import { dsError } from "../util/ds_error.ts";
5
+
6
+ export type R2Config = {
7
+ accountId: string;
8
+ bucket: string;
9
+ accessKeyId: string;
10
+ secretAccessKey: string;
11
+ region?: string;
12
+ };
13
+
14
+ function sha256Hex(data: Uint8Array | string): string {
15
+ return createHash("sha256").update(data).digest("hex");
16
+ }
17
+
18
+ async function sha256FileHex(path: string): Promise<string> {
19
+ const hash = createHash("sha256");
20
+ await new Promise<void>((resolve, reject) => {
21
+ const stream = createReadStream(path);
22
+ stream.on("data", (chunk) => hash.update(chunk));
23
+ stream.on("error", (err) => reject(err));
24
+ stream.on("end", () => resolve());
25
+ });
26
+ return hash.digest("hex");
27
+ }
28
+
29
+ function stripQuotes(value: string | null): string {
30
+ if (!value) return "";
31
+ return value.replace(/^\"|\"$/g, "");
32
+ }
33
+
34
+ export class R2ObjectStore implements ObjectStore {
35
+ private readonly client: Bun.S3Client;
36
+
37
+ constructor(cfg: R2Config) {
38
+ this.client = new Bun.S3Client({
39
+ bucket: cfg.bucket,
40
+ accessKeyId: cfg.accessKeyId,
41
+ secretAccessKey: cfg.secretAccessKey,
42
+ region: cfg.region ?? "auto",
43
+ endpoint: `https://${cfg.accountId}.r2.cloudflarestorage.com`,
44
+ });
45
+ }
46
+
47
+ private file(key: string): Bun.S3File {
48
+ return this.client.file(key);
49
+ }
50
+
51
+ private wrapError(op: string, key: string, err: unknown): never {
52
+ const message = String((err as any)?.message ?? err);
53
+ throw dsError(`R2 ${op} failed for ${key}: ${message}`);
54
+ }
55
+
56
+ async put(key: string, data: Uint8Array, opts: { contentType?: string; contentLength?: number } = {}): Promise<PutResult> {
57
+ try {
58
+ await this.file(key).write(data, { type: opts.contentType });
59
+ const stat = await this.file(key).stat();
60
+ return { etag: stripQuotes(stat.etag) || sha256Hex(data) };
61
+ } catch (err) {
62
+ this.wrapError("PUT", key, err);
63
+ }
64
+ }
65
+
66
+ async putFile(key: string, path: string, _size: number, opts: { contentType?: string } = {}): Promise<PutResult> {
67
+ try {
68
+ await this.file(key).write(Bun.file(path), { type: opts.contentType });
69
+ const stat = await this.file(key).stat();
70
+ return { etag: stripQuotes(stat.etag) || (await sha256FileHex(path)) };
71
+ } catch (err) {
72
+ this.wrapError("PUT", key, err);
73
+ }
74
+ }
75
+
76
+ async get(key: string, opts: GetOptions = {}): Promise<Uint8Array | null> {
77
+ try {
78
+ const file = this.file(key);
79
+ if (!(await file.exists())) return null;
80
+ const body =
81
+ opts.range == null
82
+ ? file
83
+ : file.slice(opts.range.start, opts.range.end == null ? undefined : opts.range.end + 1);
84
+ return new Uint8Array(await body.arrayBuffer());
85
+ } catch (err) {
86
+ this.wrapError("GET", key, err);
87
+ }
88
+ }
89
+
90
+ async head(key: string): Promise<{ etag: string; size: number } | null> {
91
+ try {
92
+ const file = this.file(key);
93
+ if (!(await file.exists())) return null;
94
+ const stat = await file.stat();
95
+ return { etag: stripQuotes(stat.etag), size: stat.size };
96
+ } catch (err) {
97
+ this.wrapError("HEAD", key, err);
98
+ }
99
+ }
100
+
101
+ async delete(key: string): Promise<void> {
102
+ try {
103
+ const file = this.file(key);
104
+ if (!(await file.exists())) return;
105
+ await file.delete();
106
+ } catch (err) {
107
+ this.wrapError("DELETE", key, err);
108
+ }
109
+ }
110
+
111
+ async list(prefix: string): Promise<string[]> {
112
+ try {
113
+ const keys: string[] = [];
114
+ let continuationToken: string | undefined;
115
+ for (;;) {
116
+ const res = await this.client.list({ prefix, continuationToken });
117
+ for (const entry of res.contents ?? []) {
118
+ keys.push(entry.key);
119
+ }
120
+ if (!res.isTruncated || !res.nextContinuationToken) break;
121
+ continuationToken = res.nextContinuationToken;
122
+ }
123
+ return keys;
124
+ } catch (err) {
125
+ this.wrapError("LIST", prefix, err);
126
+ }
127
+ }
128
+ }
package/src/offset.ts ADDED
@@ -0,0 +1,70 @@
1
+ import { decodeCrockfordBase32Fixed26Result, encodeCrockfordBase32Fixed26Result } from "./util/base32_crockford";
2
+ import { readU32BE, writeU32BE } from "./util/endian";
3
+ import { Result } from "better-result";
4
+ import { dsError } from "./util/ds_error.ts";
5
+
6
+ export type ParsedOffset =
7
+ | { kind: "start" }
8
+ | { kind: "seq"; epoch: number; seq: bigint; inBlock: number };
9
+
10
+ const DEFAULT_EPOCH = 0;
11
+
12
+ export function parseOffsetResult(input: string | null | undefined): Result<ParsedOffset, { kind: "invalid_offset"; message: string }> {
13
+ if (input == null || input === "") {
14
+ return Result.err({ kind: "invalid_offset", message: "missing offset" });
15
+ }
16
+ if (input === "-1") return Result.ok({ kind: "start" });
17
+
18
+ if (input.length !== 26) {
19
+ return Result.err({ kind: "invalid_offset", message: `invalid offset length: ${input.length}` });
20
+ }
21
+
22
+ const bytesRes = decodeCrockfordBase32Fixed26Result(input);
23
+ if (Result.isError(bytesRes)) return Result.err({ kind: "invalid_offset", message: bytesRes.error.message });
24
+ const bytes = bytesRes.value;
25
+ const epoch = readU32BE(bytes, 0);
26
+ const hi = readU32BE(bytes, 4);
27
+ const lo = readU32BE(bytes, 8);
28
+ const inBlock = readU32BE(bytes, 12);
29
+ // Protocol offsets are shifted by +1 so we never emit reserved "-1".
30
+ const rawSeq = (BigInt(hi) << 32n) | BigInt(lo);
31
+ const seq = rawSeq - 1n;
32
+ return Result.ok({ kind: "seq", epoch, seq, inBlock });
33
+ }
34
+
35
+ export function parseOffset(input: string | null | undefined): ParsedOffset {
36
+ const res = parseOffsetResult(input);
37
+ if (Result.isError(res)) throw dsError(res.error.message);
38
+ return res.value;
39
+ }
40
+
41
+ export function encodeOffsetResult(epoch: number, seq: bigint, inBlock = 0): Result<string, { kind: "invalid_offset"; message: string }> {
42
+ if (seq < -1n) return Result.err({ kind: "invalid_offset", message: "invalid offset" });
43
+ const bytes = new Uint8Array(16);
44
+ writeU32BE(bytes, 0, epoch >>> 0);
45
+ const rawSeq = seq + 1n;
46
+ const hi = Number((rawSeq >> 32n) & 0xffffffffn);
47
+ const lo = Number(rawSeq & 0xffffffffn);
48
+ writeU32BE(bytes, 4, hi);
49
+ writeU32BE(bytes, 8, lo);
50
+ writeU32BE(bytes, 12, inBlock >>> 0);
51
+ const encodedRes = encodeCrockfordBase32Fixed26Result(bytes);
52
+ if (Result.isError(encodedRes)) return Result.err({ kind: "invalid_offset", message: encodedRes.error.message });
53
+ return Result.ok(encodedRes.value);
54
+ }
55
+
56
+ export function encodeOffset(epoch: number, seq: bigint, inBlock = 0): string {
57
+ const res = encodeOffsetResult(epoch, seq, inBlock);
58
+ if (Result.isError(res)) throw dsError(res.error.message);
59
+ return res.value;
60
+ }
61
+
62
+ export function canonicalizeOffset(input: string): string {
63
+ const p = parseOffset(input);
64
+ if (p.kind === "start") return encodeOffset(DEFAULT_EPOCH, -1n, 0);
65
+ return encodeOffset(p.epoch, p.seq, p.inBlock);
66
+ }
67
+
68
+ export function offsetToSeqOrNeg1(p: ParsedOffset): bigint {
69
+ return p.kind === "start" ? -1n : p.seq;
70
+ }