@syncular/server 0.0.1 → 0.0.2-127

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 (171) hide show
  1. package/README.md +25 -0
  2. package/dist/blobs/adapters/database.d.ts.map +1 -1
  3. package/dist/blobs/adapters/database.js +25 -3
  4. package/dist/blobs/adapters/database.js.map +1 -1
  5. package/dist/blobs/adapters/filesystem.d.ts +31 -0
  6. package/dist/blobs/adapters/filesystem.d.ts.map +1 -0
  7. package/dist/blobs/adapters/filesystem.js +140 -0
  8. package/dist/blobs/adapters/filesystem.js.map +1 -0
  9. package/dist/blobs/adapters/s3.d.ts +3 -2
  10. package/dist/blobs/adapters/s3.d.ts.map +1 -1
  11. package/dist/blobs/adapters/s3.js +49 -0
  12. package/dist/blobs/adapters/s3.js.map +1 -1
  13. package/dist/blobs/index.d.ts +1 -0
  14. package/dist/blobs/index.d.ts.map +1 -1
  15. package/dist/blobs/index.js +6 -5
  16. package/dist/blobs/index.js.map +1 -1
  17. package/dist/clients.d.ts +1 -0
  18. package/dist/clients.d.ts.map +1 -1
  19. package/dist/clients.js.map +1 -1
  20. package/dist/compaction.d.ts +1 -1
  21. package/dist/compaction.js +1 -1
  22. package/dist/dialect/base.d.ts +83 -0
  23. package/dist/dialect/base.d.ts.map +1 -0
  24. package/dist/dialect/base.js +144 -0
  25. package/dist/dialect/base.js.map +1 -0
  26. package/dist/dialect/helpers.d.ts +10 -0
  27. package/dist/dialect/helpers.d.ts.map +1 -0
  28. package/dist/dialect/helpers.js +59 -0
  29. package/dist/dialect/helpers.js.map +1 -0
  30. package/dist/dialect/index.d.ts +2 -0
  31. package/dist/dialect/index.d.ts.map +1 -1
  32. package/dist/dialect/index.js +3 -1
  33. package/dist/dialect/index.js.map +1 -1
  34. package/dist/dialect/types.d.ts +38 -46
  35. package/dist/dialect/types.d.ts.map +1 -1
  36. package/dist/{shapes → handlers}/create-handler.d.ts +18 -5
  37. package/dist/handlers/create-handler.d.ts.map +1 -0
  38. package/dist/{shapes → handlers}/create-handler.js +140 -43
  39. package/dist/handlers/create-handler.js.map +1 -0
  40. package/dist/handlers/index.d.ts.map +1 -0
  41. package/dist/handlers/index.js +4 -0
  42. package/dist/handlers/index.js.map +1 -0
  43. package/dist/handlers/registry.d.ts.map +1 -0
  44. package/dist/handlers/registry.js.map +1 -0
  45. package/dist/{shapes → handlers}/types.d.ts +7 -7
  46. package/dist/{shapes → handlers}/types.d.ts.map +1 -1
  47. package/dist/{shapes → handlers}/types.js.map +1 -1
  48. package/dist/helpers/conflict.d.ts +1 -1
  49. package/dist/helpers/conflict.d.ts.map +1 -1
  50. package/dist/helpers/emitted-change.d.ts +1 -1
  51. package/dist/helpers/emitted-change.d.ts.map +1 -1
  52. package/dist/helpers/index.js +4 -4
  53. package/dist/index.d.ts +2 -1
  54. package/dist/index.d.ts.map +1 -1
  55. package/dist/index.js +17 -16
  56. package/dist/index.js.map +1 -1
  57. package/dist/notify.d.ts +47 -0
  58. package/dist/notify.d.ts.map +1 -0
  59. package/dist/notify.js +85 -0
  60. package/dist/notify.js.map +1 -0
  61. package/dist/proxy/handler.d.ts +1 -1
  62. package/dist/proxy/handler.d.ts.map +1 -1
  63. package/dist/proxy/handler.js +15 -11
  64. package/dist/proxy/handler.js.map +1 -1
  65. package/dist/proxy/index.d.ts +2 -2
  66. package/dist/proxy/index.d.ts.map +1 -1
  67. package/dist/proxy/index.js +3 -3
  68. package/dist/proxy/index.js.map +1 -1
  69. package/dist/proxy/mutation-detector.d.ts +4 -0
  70. package/dist/proxy/mutation-detector.d.ts.map +1 -1
  71. package/dist/proxy/mutation-detector.js +209 -24
  72. package/dist/proxy/mutation-detector.js.map +1 -1
  73. package/dist/proxy/oplog.d.ts +2 -1
  74. package/dist/proxy/oplog.d.ts.map +1 -1
  75. package/dist/proxy/oplog.js +15 -9
  76. package/dist/proxy/oplog.js.map +1 -1
  77. package/dist/proxy/registry.d.ts +0 -11
  78. package/dist/proxy/registry.d.ts.map +1 -1
  79. package/dist/proxy/registry.js +0 -24
  80. package/dist/proxy/registry.js.map +1 -1
  81. package/dist/proxy/types.d.ts +2 -0
  82. package/dist/proxy/types.d.ts.map +1 -1
  83. package/dist/pull.d.ts +4 -3
  84. package/dist/pull.d.ts.map +1 -1
  85. package/dist/pull.js +565 -314
  86. package/dist/pull.js.map +1 -1
  87. package/dist/push.d.ts +15 -3
  88. package/dist/push.d.ts.map +1 -1
  89. package/dist/push.js +359 -229
  90. package/dist/push.js.map +1 -1
  91. package/dist/realtime/index.js +1 -1
  92. package/dist/realtime/types.d.ts +2 -0
  93. package/dist/realtime/types.d.ts.map +1 -1
  94. package/dist/schema.d.ts +11 -1
  95. package/dist/schema.d.ts.map +1 -1
  96. package/dist/snapshot-chunks/db-metadata.d.ts +6 -1
  97. package/dist/snapshot-chunks/db-metadata.d.ts.map +1 -1
  98. package/dist/snapshot-chunks/db-metadata.js +261 -92
  99. package/dist/snapshot-chunks/db-metadata.js.map +1 -1
  100. package/dist/snapshot-chunks/index.d.ts +0 -1
  101. package/dist/snapshot-chunks/index.d.ts.map +1 -1
  102. package/dist/snapshot-chunks/index.js +2 -3
  103. package/dist/snapshot-chunks/index.js.map +1 -1
  104. package/dist/snapshot-chunks/types.d.ts +20 -5
  105. package/dist/snapshot-chunks/types.d.ts.map +1 -1
  106. package/dist/snapshot-chunks.d.ts +12 -8
  107. package/dist/snapshot-chunks.d.ts.map +1 -1
  108. package/dist/snapshot-chunks.js +40 -12
  109. package/dist/snapshot-chunks.js.map +1 -1
  110. package/dist/subscriptions/index.js +1 -1
  111. package/dist/subscriptions/resolve.d.ts +6 -6
  112. package/dist/subscriptions/resolve.d.ts.map +1 -1
  113. package/dist/subscriptions/resolve.js +53 -14
  114. package/dist/subscriptions/resolve.js.map +1 -1
  115. package/package.json +28 -7
  116. package/src/blobs/adapters/database.test.ts +67 -0
  117. package/src/blobs/adapters/database.ts +34 -9
  118. package/src/blobs/adapters/filesystem.test.ts +132 -0
  119. package/src/blobs/adapters/filesystem.ts +189 -0
  120. package/src/blobs/adapters/s3.test.ts +522 -0
  121. package/src/blobs/adapters/s3.ts +55 -2
  122. package/src/blobs/index.ts +1 -0
  123. package/src/clients.ts +1 -0
  124. package/src/compaction.ts +1 -1
  125. package/src/dialect/base.ts +292 -0
  126. package/src/dialect/helpers.ts +61 -0
  127. package/src/dialect/index.ts +2 -0
  128. package/src/dialect/types.ts +50 -54
  129. package/src/{shapes → handlers}/create-handler.ts +219 -64
  130. package/src/{shapes → handlers}/types.ts +10 -7
  131. package/src/helpers/conflict.ts +1 -1
  132. package/src/helpers/emitted-change.ts +1 -1
  133. package/src/index.ts +2 -1
  134. package/src/notify.test.ts +516 -0
  135. package/src/notify.ts +131 -0
  136. package/src/proxy/handler.test.ts +120 -0
  137. package/src/proxy/handler.ts +18 -10
  138. package/src/proxy/index.ts +2 -1
  139. package/src/proxy/mutation-detector.test.ts +71 -0
  140. package/src/proxy/mutation-detector.ts +227 -29
  141. package/src/proxy/oplog.ts +19 -10
  142. package/src/proxy/registry.ts +0 -33
  143. package/src/proxy/types.ts +2 -0
  144. package/src/pull.ts +788 -405
  145. package/src/push.ts +507 -312
  146. package/src/realtime/types.ts +2 -0
  147. package/src/schema.ts +11 -1
  148. package/src/snapshot-chunks/db-metadata.test.ts +169 -0
  149. package/src/snapshot-chunks/db-metadata.ts +347 -105
  150. package/src/snapshot-chunks/index.ts +0 -1
  151. package/src/snapshot-chunks/types.ts +31 -5
  152. package/src/snapshot-chunks.ts +60 -21
  153. package/src/subscriptions/resolve.ts +73 -18
  154. package/dist/shapes/create-handler.d.ts.map +0 -1
  155. package/dist/shapes/create-handler.js.map +0 -1
  156. package/dist/shapes/index.d.ts.map +0 -1
  157. package/dist/shapes/index.js +0 -4
  158. package/dist/shapes/index.js.map +0 -1
  159. package/dist/shapes/registry.d.ts.map +0 -1
  160. package/dist/shapes/registry.js.map +0 -1
  161. package/dist/snapshot-chunks/adapters/s3.d.ts +0 -63
  162. package/dist/snapshot-chunks/adapters/s3.d.ts.map +0 -1
  163. package/dist/snapshot-chunks/adapters/s3.js +0 -50
  164. package/dist/snapshot-chunks/adapters/s3.js.map +0 -1
  165. package/src/snapshot-chunks/adapters/s3.ts +0 -68
  166. /package/dist/{shapes → handlers}/index.d.ts +0 -0
  167. /package/dist/{shapes → handlers}/registry.d.ts +0 -0
  168. /package/dist/{shapes → handlers}/registry.js +0 -0
  169. /package/dist/{shapes → handlers}/types.js +0 -0
  170. /package/src/{shapes → handlers}/index.ts +0 -0
  171. /package/src/{shapes → handlers}/registry.ts +0 -0
@@ -46,15 +46,16 @@ export interface BlobTokenSigner {
46
46
  */
47
47
  export function createHmacTokenSigner(secret: string): BlobTokenSigner {
48
48
  const encoder = new TextEncoder();
49
+ const keyPromise = crypto.subtle.importKey(
50
+ 'raw',
51
+ encoder.encode(secret),
52
+ { name: 'HMAC', hash: 'SHA-256' },
53
+ false,
54
+ ['sign', 'verify']
55
+ );
49
56
 
50
57
  async function hmacSign(data: string): Promise<string> {
51
- const key = await crypto.subtle.importKey(
52
- 'raw',
53
- encoder.encode(secret),
54
- { name: 'HMAC', hash: 'SHA-256' },
55
- false,
56
- ['sign']
57
- );
58
+ const key = await keyPromise;
58
59
  const signature = await crypto.subtle.sign(
59
60
  'HMAC',
60
61
  key,
@@ -63,6 +64,18 @@ export function createHmacTokenSigner(secret: string): BlobTokenSigner {
63
64
  return bufferToHex(new Uint8Array(signature));
64
65
  }
65
66
 
67
+ async function hmacVerify(
68
+ data: string,
69
+ signatureHex: string
70
+ ): Promise<boolean> {
71
+ const parsedSignature = hexToBuffer(signatureHex);
72
+ if (!parsedSignature) return false;
73
+ const signature = new Uint8Array(parsedSignature.length);
74
+ signature.set(parsedSignature);
75
+ const key = await keyPromise;
76
+ return crypto.subtle.verify('HMAC', key, signature, encoder.encode(data));
77
+ }
78
+
66
79
  return {
67
80
  async sign(payload, _expiresIn) {
68
81
  const data = JSON.stringify(payload);
@@ -75,8 +88,8 @@ export function createHmacTokenSigner(secret: string): BlobTokenSigner {
75
88
  const [dataB64, sig] = token.split('.');
76
89
  if (!dataB64 || !sig) return null;
77
90
 
78
- const expectedSig = await hmacSign(dataB64);
79
- if (sig !== expectedSig) return null;
91
+ const isValidSig = await hmacVerify(dataB64, sig);
92
+ if (!isValidSig) return null;
80
93
 
81
94
  try {
82
95
  const data = JSON.parse(atob(dataB64)) as {
@@ -101,6 +114,18 @@ function bufferToHex(buffer: Uint8Array): string {
101
114
  .join('');
102
115
  }
103
116
 
117
+ function hexToBuffer(hex: string): Uint8Array | null {
118
+ if (hex.length === 0 || hex.length % 2 !== 0) return null;
119
+ if (!/^[0-9a-f]+$/i.test(hex)) return null;
120
+
121
+ const out = new Uint8Array(hex.length / 2);
122
+ for (let i = 0; i < hex.length; i += 2) {
123
+ const pair = hex.slice(i, i + 2);
124
+ out[i / 2] = Number.parseInt(pair, 16);
125
+ }
126
+ return out;
127
+ }
128
+
104
129
  export interface DatabaseBlobStorageAdapterOptions<
105
130
  DB extends SyncBlobsDb = SyncBlobsDb,
106
131
  > {
@@ -0,0 +1,132 @@
1
+ import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
2
+ import { mkdtemp, readdir, rm } from 'node:fs/promises';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import type { BlobStorageAdapter } from '@syncular/core';
6
+ import { createHmacTokenSigner } from './database';
7
+ import { createFilesystemBlobStorageAdapter } from './filesystem';
8
+
9
+ let basePath: string;
10
+ let adapter: BlobStorageAdapter;
11
+
12
+ beforeEach(async () => {
13
+ basePath = await mkdtemp(join(tmpdir(), 'syncular-blob-test-'));
14
+ adapter = createFilesystemBlobStorageAdapter({
15
+ basePath,
16
+ baseUrl: 'https://example.com/api/sync',
17
+ tokenSigner: createHmacTokenSigner('test-secret'),
18
+ });
19
+ });
20
+
21
+ afterEach(async () => {
22
+ await rm(basePath, { recursive: true, force: true });
23
+ });
24
+
25
+ const testHash =
26
+ 'sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890';
27
+ const testData = new TextEncoder().encode('hello world');
28
+
29
+ describe('createFilesystemBlobStorageAdapter', () => {
30
+ test('put + get round-trip', async () => {
31
+ await adapter.put!(testHash, testData);
32
+ const result = await adapter.get!(testHash);
33
+ expect(result).toEqual(testData);
34
+ });
35
+
36
+ test('putStream + getStream round-trip', async () => {
37
+ const inputStream = new ReadableStream<Uint8Array>({
38
+ start(controller) {
39
+ controller.enqueue(testData);
40
+ controller.close();
41
+ },
42
+ });
43
+
44
+ await adapter.putStream!(testHash, inputStream);
45
+
46
+ const outputStream = await adapter.getStream!(testHash);
47
+ expect(outputStream).not.toBeNull();
48
+
49
+ const reader = outputStream!.getReader();
50
+ const chunks: Uint8Array[] = [];
51
+ while (true) {
52
+ const { done, value } = await reader.read();
53
+ if (done) break;
54
+ chunks.push(value);
55
+ }
56
+ const result = new Uint8Array(chunks.reduce((acc, c) => acc + c.length, 0));
57
+ let offset = 0;
58
+ for (const chunk of chunks) {
59
+ result.set(chunk, offset);
60
+ offset += chunk.length;
61
+ }
62
+ expect(result).toEqual(testData);
63
+ });
64
+
65
+ test('get returns null for missing blob', async () => {
66
+ const result = await adapter.get!(testHash);
67
+ expect(result).toBeNull();
68
+ });
69
+
70
+ test('getStream returns null for missing blob', async () => {
71
+ const result = await adapter.getStream!(testHash);
72
+ expect(result).toBeNull();
73
+ });
74
+
75
+ test('exists returns true after put', async () => {
76
+ expect(await adapter.exists(testHash)).toBe(false);
77
+ await adapter.put!(testHash, testData);
78
+ expect(await adapter.exists(testHash)).toBe(true);
79
+ });
80
+
81
+ test('delete removes the blob', async () => {
82
+ await adapter.put!(testHash, testData);
83
+ expect(await adapter.exists(testHash)).toBe(true);
84
+ await adapter.delete(testHash);
85
+ expect(await adapter.exists(testHash)).toBe(false);
86
+ });
87
+
88
+ test('delete is idempotent for missing blob', async () => {
89
+ await adapter.delete(testHash);
90
+ });
91
+
92
+ test('getMetadata returns size', async () => {
93
+ await adapter.put!(testHash, testData);
94
+ const meta = await adapter.getMetadata!(testHash);
95
+ expect(meta).toEqual({ size: testData.length });
96
+ });
97
+
98
+ test('getMetadata returns null for missing blob', async () => {
99
+ const meta = await adapter.getMetadata!(testHash);
100
+ expect(meta).toBeNull();
101
+ });
102
+
103
+ test('creates hash-based subdirectories', async () => {
104
+ await adapter.put!(testHash, testData);
105
+ // hex = abcdef..., so subdirs should be "ab/cd"
106
+ const firstLevel = await readdir(basePath);
107
+ expect(firstLevel).toContain('ab');
108
+ const secondLevel = await readdir(join(basePath, 'ab'));
109
+ expect(secondLevel).toContain('cd');
110
+ });
111
+
112
+ test('signUpload returns a token URL', async () => {
113
+ const result = await adapter.signUpload({
114
+ hash: testHash,
115
+ size: 100,
116
+ mimeType: 'application/octet-stream',
117
+ expiresIn: 60,
118
+ });
119
+ expect(result.url).toContain('/blobs/');
120
+ expect(result.url).toContain('token=');
121
+ expect(result.method).toBe('PUT');
122
+ });
123
+
124
+ test('signDownload returns a token URL', async () => {
125
+ const url = await adapter.signDownload({
126
+ hash: testHash,
127
+ expiresIn: 60,
128
+ });
129
+ expect(url).toContain('/blobs/');
130
+ expect(url).toContain('token=');
131
+ });
132
+ });
@@ -0,0 +1,189 @@
1
+ /**
2
+ * Filesystem blob storage adapter.
3
+ *
4
+ * Stores blobs as files on disk with 2-level hash-based subdirectories.
5
+ * Uploads/downloads go through the server's blob routes using signed tokens
6
+ * (same pattern as the database adapter).
7
+ */
8
+
9
+ import {
10
+ mkdir,
11
+ open,
12
+ readFile,
13
+ rename,
14
+ stat,
15
+ unlink,
16
+ writeFile,
17
+ } from 'node:fs/promises';
18
+ import { dirname, join } from 'node:path';
19
+ import type {
20
+ BlobSignDownloadOptions,
21
+ BlobSignedUpload,
22
+ BlobSignUploadOptions,
23
+ BlobStorageAdapter,
24
+ } from '@syncular/core';
25
+ import type { BlobTokenSigner } from './database';
26
+
27
+ export interface FilesystemBlobStorageAdapterOptions {
28
+ /** Directory root for blob files */
29
+ basePath: string;
30
+ /** Server base URL for upload/download routes (e.g. "/api/sync") */
31
+ baseUrl: string;
32
+ /** Token signer for authorization */
33
+ tokenSigner: BlobTokenSigner;
34
+ }
35
+
36
+ /**
37
+ * Resolve hash to a 2-level subdirectory path:
38
+ * `{basePath}/{hex[0..2]}/{hex[2..4]}/{hex}`
39
+ */
40
+ function hashToFilePath(basePath: string, hash: string): string {
41
+ const hex = hash.startsWith('sha256:') ? hash.slice(7) : hash;
42
+ return join(basePath, hex.slice(0, 2), hex.slice(2, 4), hex);
43
+ }
44
+
45
+ function tmpPath(filePath: string): string {
46
+ return `${filePath}.${Date.now()}.tmp`;
47
+ }
48
+
49
+ /**
50
+ * Create a filesystem blob storage adapter.
51
+ *
52
+ * @example
53
+ * ```typescript
54
+ * const adapter = createFilesystemBlobStorageAdapter({
55
+ * basePath: '/data/blobs',
56
+ * baseUrl: 'https://api.example.com/api/sync',
57
+ * tokenSigner: createHmacTokenSigner(process.env.BLOB_SECRET!),
58
+ * });
59
+ * ```
60
+ */
61
+ export function createFilesystemBlobStorageAdapter(
62
+ options: FilesystemBlobStorageAdapterOptions
63
+ ): BlobStorageAdapter {
64
+ const { basePath, tokenSigner } = options;
65
+ const normalizedBaseUrl = options.baseUrl.replace(/\/$/, '');
66
+
67
+ return {
68
+ name: 'filesystem',
69
+
70
+ async signUpload(opts: BlobSignUploadOptions): Promise<BlobSignedUpload> {
71
+ const expiresAt = Date.now() + opts.expiresIn * 1000;
72
+ const token = await tokenSigner.sign(
73
+ { hash: opts.hash, action: 'upload', expiresAt },
74
+ opts.expiresIn
75
+ );
76
+
77
+ const url = `${normalizedBaseUrl}/blobs/${encodeURIComponent(opts.hash)}/upload?token=${encodeURIComponent(token)}`;
78
+
79
+ return {
80
+ url,
81
+ method: 'PUT',
82
+ headers: {
83
+ 'Content-Type': opts.mimeType,
84
+ 'Content-Length': String(opts.size),
85
+ },
86
+ };
87
+ },
88
+
89
+ async signDownload(opts: BlobSignDownloadOptions): Promise<string> {
90
+ const expiresAt = Date.now() + opts.expiresIn * 1000;
91
+ const token = await tokenSigner.sign(
92
+ { hash: opts.hash, action: 'download', expiresAt },
93
+ opts.expiresIn
94
+ );
95
+
96
+ return `${normalizedBaseUrl}/blobs/${encodeURIComponent(opts.hash)}/download?token=${encodeURIComponent(token)}`;
97
+ },
98
+
99
+ async exists(hash: string): Promise<boolean> {
100
+ try {
101
+ await stat(hashToFilePath(basePath, hash));
102
+ return true;
103
+ } catch {
104
+ return false;
105
+ }
106
+ },
107
+
108
+ async delete(hash: string): Promise<void> {
109
+ try {
110
+ await unlink(hashToFilePath(basePath, hash));
111
+ } catch (err) {
112
+ if ((err as NodeJS.ErrnoException).code !== 'ENOENT') throw err;
113
+ }
114
+ },
115
+
116
+ async getMetadata(
117
+ hash: string
118
+ ): Promise<{ size: number; mimeType?: string } | null> {
119
+ try {
120
+ const s = await stat(hashToFilePath(basePath, hash));
121
+ return { size: s.size };
122
+ } catch {
123
+ return null;
124
+ }
125
+ },
126
+
127
+ async put(hash: string, data: Uint8Array): Promise<void> {
128
+ const filePath = hashToFilePath(basePath, hash);
129
+ const tmp = tmpPath(filePath);
130
+ await mkdir(dirname(filePath), { recursive: true });
131
+ await writeFile(tmp, data);
132
+ await rename(tmp, filePath);
133
+ },
134
+
135
+ async putStream(
136
+ hash: string,
137
+ stream: ReadableStream<Uint8Array>
138
+ ): Promise<void> {
139
+ const filePath = hashToFilePath(basePath, hash);
140
+ const tmp = tmpPath(filePath);
141
+ await mkdir(dirname(filePath), { recursive: true });
142
+
143
+ const fh = await open(tmp, 'w');
144
+ try {
145
+ const reader = stream.getReader();
146
+ while (true) {
147
+ const { done, value } = await reader.read();
148
+ if (done) break;
149
+ await fh.write(value);
150
+ }
151
+ } finally {
152
+ await fh.close();
153
+ }
154
+
155
+ await rename(tmp, filePath);
156
+ },
157
+
158
+ async get(hash: string): Promise<Uint8Array | null> {
159
+ try {
160
+ const buf = await readFile(hashToFilePath(basePath, hash));
161
+ return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
162
+ } catch (err) {
163
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
164
+ throw err;
165
+ }
166
+ },
167
+
168
+ async getStream(hash: string): Promise<ReadableStream<Uint8Array> | null> {
169
+ let data: Buffer;
170
+ try {
171
+ data = await readFile(hashToFilePath(basePath, hash));
172
+ } catch (err) {
173
+ if ((err as NodeJS.ErrnoException).code === 'ENOENT') return null;
174
+ throw err;
175
+ }
176
+ const bytes = new Uint8Array(
177
+ data.buffer,
178
+ data.byteOffset,
179
+ data.byteLength
180
+ );
181
+ return new ReadableStream<Uint8Array>({
182
+ start(controller) {
183
+ controller.enqueue(bytes);
184
+ controller.close();
185
+ },
186
+ });
187
+ },
188
+ };
189
+ }