@agentunion/fastaun 0.5.21 → 0.5.23

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 (75) hide show
  1. package/CHANGELOG.md +58 -1
  2. package/_packed_docs/CHANGELOG.md +58 -1
  3. package/_packed_docs/INDEX.md +469 -453
  4. package/_packed_docs/KITE_DOCS_GUIDE.md +169 -164
  5. package/_packed_docs/aun/345/210/206/345/270/203/345/274/217/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +307 -67
  6. package/_packed_docs/aun/346/265/213/350/257/225/350/277/220/350/241/214/346/214/207/345/215/227.md +41 -15
  7. package/_packed_docs/protocol/06-/346/234/215/345/212/241/345/215/217/350/256/256.md +5 -5
  8. package/_packed_docs/protocol/11-Storage-/345/255/220/345/215/217/350/256/256.md +30 -8
  9. package/_packed_docs/protocol/Ownership/345/215/217/350/256/256/345/256/232/344/271/2112.1.md +669 -666
  10. package/_packed_docs/sdk/0.5.22-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +212 -0
  11. package/_packed_docs/sdk/0.5.22-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +199 -0
  12. package/_packed_docs/sdk/06-API/346/211/213/345/206/214.md +16 -0
  13. package/_packed_docs/sdk/09-group-rpc-manual.md +10 -0
  14. package/_packed_docs/sdk/09-storage-rpc-manual.md +64 -7
  15. package/_packed_docs/sdk/09-/345/207/255/350/257/201/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +3 -1
  16. package/_packed_docs/sdk/09-/347/273/204/347/273/207/346/234/215/345/212/241RPC/346/211/213/345/206/214.md +2 -0
  17. package/_packed_docs/sdk/AUN_DOCS_GUIDE.md +138 -134
  18. package/_packed_docs/sdk/INDEX.md +255 -239
  19. package/_packed_docs/sdk/Ownership-SDK/347/274/226/346/216/222/346/214/207/345/215/2272.1.md +939 -930
  20. package/_packed_docs/sdk/Ownership2.1-SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +312 -363
  21. package/_packed_docs/sdk/Ownership2.1-SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +322 -553
  22. package/_packed_docs/sdk/Ownership2.1-TDD/345/210/206/351/230/266/346/256/265/345/256/236/346/226/275/350/256/241/345/210/222.md +4 -4
  23. package/_packed_docs/sdk/Ownership2.1/350/257/246/347/273/206/350/256/276/350/256/241.md +484 -480
  24. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/272/224/347/224/250/351/233/206/346/210/220/346/214/207/345/215/227.md +190 -97
  25. package/_packed_docs/sdk//345/207/255/350/257/201/344/270/216/347/273/204/347/273/207/346/234/215/345/212/241SDK/345/274/200/345/217/221/344/270/216/347/247/273/346/244/215/346/214/207/345/215/227.md +158 -127
  26. package/dist/agent-md.d.ts +16 -2
  27. package/dist/agent-md.js +59 -23
  28. package/dist/agent-md.js.map +1 -1
  29. package/dist/aid-store.d.ts +1 -0
  30. package/dist/aid-store.js +43 -8
  31. package/dist/aid-store.js.map +1 -1
  32. package/dist/auth.d.ts +7 -0
  33. package/dist/auth.js +222 -70
  34. package/dist/auth.js.map +1 -1
  35. package/dist/client/delivery.d.ts +28 -0
  36. package/dist/client/delivery.js +399 -44
  37. package/dist/client/delivery.js.map +1 -1
  38. package/dist/client/group-state.js +3 -0
  39. package/dist/client/group-state.js.map +1 -1
  40. package/dist/client/rpc-pipeline.js +28 -2
  41. package/dist/client/rpc-pipeline.js.map +1 -1
  42. package/dist/client/v2-e2ee.js +109 -39
  43. package/dist/client/v2-e2ee.js.map +1 -1
  44. package/dist/client.d.ts +3 -0
  45. package/dist/client.js +114 -26
  46. package/dist/client.js.map +1 -1
  47. package/dist/group-fs.d.ts +1 -0
  48. package/dist/group-fs.js +37 -19
  49. package/dist/group-fs.js.map +1 -1
  50. package/dist/org-protocol.d.ts +11 -0
  51. package/dist/org-protocol.js +154 -19
  52. package/dist/org-protocol.js.map +1 -1
  53. package/dist/ownership-v21.d.ts +21 -3
  54. package/dist/ownership-v21.js +135 -37
  55. package/dist/ownership-v21.js.map +1 -1
  56. package/dist/ownership-workflow.d.ts +7 -2
  57. package/dist/ownership-workflow.js +173 -17
  58. package/dist/ownership-workflow.js.map +1 -1
  59. package/dist/storage/lowlevel.d.ts +6 -0
  60. package/dist/storage/lowlevel.js +24 -7
  61. package/dist/storage/lowlevel.js.map +1 -1
  62. package/dist/storage/read-cache.d.ts +70 -0
  63. package/dist/storage/read-cache.js +625 -0
  64. package/dist/storage/read-cache.js.map +1 -0
  65. package/dist/storage/vfs.d.ts +2 -0
  66. package/dist/storage/vfs.js +50 -15
  67. package/dist/storage/vfs.js.map +1 -1
  68. package/dist/tools/cross-sdk-agent.d.ts +21 -1
  69. package/dist/tools/cross-sdk-agent.js +301 -19
  70. package/dist/tools/cross-sdk-agent.js.map +1 -1
  71. package/dist/transport.js +6 -2
  72. package/dist/transport.js.map +1 -1
  73. package/dist/version.d.ts +1 -1
  74. package/dist/version.js +1 -1
  75. package/package.json +1 -1
@@ -0,0 +1,625 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { createRequire } from 'node:module';
3
+ import { AsyncLocalStorage } from 'node:async_hooks';
4
+ import { mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
5
+ import { mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs';
6
+ import { homedir } from 'node:os';
7
+ import { join, resolve } from 'node:path';
8
+ import { StorageError } from './errors.js';
9
+ const { DatabaseSync } = createRequire(import.meta.url)('node:sqlite');
10
+ const METADATA_TTL_MS = 60_000;
11
+ const MEMORY_LIMIT_BYTES = 32 * 1024 * 1024;
12
+ const DISK_LIMIT_BYTES = 256 * 1024 * 1024;
13
+ const METADATA_LIMIT_BYTES = 16 * 1024 * 1024;
14
+ const MEMORY_LIMIT_COUNT = 999;
15
+ const DISK_LIMIT_COUNT = 9_999;
16
+ const caches = new WeakMap();
17
+ const metadataMethods = new Set([
18
+ 'storage.get_limits', 'storage.get_quota', 'storage.head_object',
19
+ 'storage.list_objects', 'storage.list_prefixes', 'storage.get_folder',
20
+ 'storage.list_children', 'storage.readlink', 'storage.list_acl',
21
+ 'storage.resolve_path', 'storage.fs.list', 'storage.fs.stat',
22
+ 'storage.fs.lstat', 'storage.fs.find', 'storage.fs.df',
23
+ 'group.fs.ls', 'group.fs.find', 'group.fs.stat', 'group.fs.lstat',
24
+ 'group.fs.get_acl', 'group.fs.list_acl', 'group.fs.df',
25
+ ]);
26
+ const nonMutatingMethods = new Set([
27
+ ...metadataMethods, 'storage.get_object', 'storage.get_by_share',
28
+ 'storage.check_upload', 'storage.create_upload_session',
29
+ 'storage.create_download_ticket', 'storage.check_access',
30
+ 'storage.list_tokens', 'storage.list_share_links',
31
+ 'group.fs.check_upload', 'group.fs.create_upload_session',
32
+ 'group.fs.create_download_ticket',
33
+ ]);
34
+ const groupPermissionMethods = new Set([
35
+ 'group.leave', 'group.kick', 'group.remove_member', 'group.add_member',
36
+ 'group.set_role', 'group.dissolve', 'group.transfer_owner',
37
+ 'group.ban', 'group.unban', 'group.block', 'group.unblock',
38
+ 'group.suspend', 'group.resume',
39
+ ]);
40
+ function canonical(value) {
41
+ if (Array.isArray(value))
42
+ return value.map(canonical);
43
+ if (value && typeof value === 'object') {
44
+ return Object.fromEntries(Object.entries(value).filter(([, item]) => item !== undefined)
45
+ .sort(([left], [right]) => left < right ? -1 : left > right ? 1 : 0)
46
+ .map(([key, item]) => [key, canonical(item)]));
47
+ }
48
+ return value;
49
+ }
50
+ export function storageCacheKey(method, params, loginAid = '') {
51
+ const identity = params._client_signature_identity;
52
+ const actor = String(identity?.aid ?? loginAid).trim().toLowerCase();
53
+ const businessParams = Object.fromEntries(Object.entries(params).filter(([key]) => ![
54
+ 'no_cache', 'noCache', 'if_sha256', 'client_signature', '_client_signature_identity', 'device_id', 'slot_id', '_rpc_background',
55
+ ].includes(key)));
56
+ if (method === 'storage.get_object' || method === 'storage.create_download_ticket') {
57
+ businessParams.owner_aid ||= actor;
58
+ businessParams.bucket ||= 'default';
59
+ }
60
+ return createHash('sha256').update(JSON.stringify(canonical({ actor, method, params: businessParams }))).digest('hex');
61
+ }
62
+ function bodyMetadata(method, value) {
63
+ const result = structuredClone(value);
64
+ for (const key of ['content', 'content_base64', 'logical_url', 'download_url', 'upload_url', 'url', 'headers', 'token', 'access_token', 'share_id', 'share_token', 'client_signature', 'not_modified'])
65
+ delete result[key];
66
+ if (method.endsWith('.create_download_ticket'))
67
+ delete result.expire_at;
68
+ return result;
69
+ }
70
+ function hasExplicitSignature(params) {
71
+ return params.client_signature !== undefined;
72
+ }
73
+ function isRangeRequest(params) {
74
+ return ['offset', 'limit', 'range_start', 'range_end', 'head_bytes'].some((key) => params[key] !== undefined);
75
+ }
76
+ export function storageReadCache(client) {
77
+ const aid = String(client.aid ?? '').trim().toLowerCase();
78
+ const aunPath = resolve(client.aunPath || join(homedir(), '.aun'));
79
+ const gateway = String(client._gatewayUrl ?? '').trim();
80
+ const scope = `${aunPath}\0${aid}\0${gateway}`;
81
+ const current = caches.get(client);
82
+ if (current?.scope === scope)
83
+ return current.cache;
84
+ current?.cache.clearMemory();
85
+ const cache = new StorageReadCache(aid ? join(aunPath, '.cache', 'storage') : undefined, aid, gateway);
86
+ caches.set(client, { scope, cache });
87
+ return cache;
88
+ }
89
+ export function invalidateStorageReadCache(client, method) {
90
+ if (!(method.startsWith('storage.') || method.startsWith('group.fs.') || groupPermissionMethods.has(method)) || nonMutatingMethods.has(method))
91
+ return;
92
+ caches.get(client)?.cache.invalidate();
93
+ }
94
+ export class StorageReadCache {
95
+ scopeDirectory;
96
+ loginAid;
97
+ epoch = 0;
98
+ metadataEntries = new Map();
99
+ pending = new Map();
100
+ metadataContext = new AsyncLocalStorage();
101
+ bodyContext = new AsyncLocalStorage();
102
+ requestSequence = 0;
103
+ activeRequests = new Map();
104
+ bodyEntries = new Map();
105
+ memoryOrder = new Set();
106
+ memoryBytes = 0;
107
+ metadataBytes = 0;
108
+ hydrated = new Set();
109
+ initialized = false;
110
+ persistenceUnavailable = false;
111
+ blobDirectory;
112
+ namespace;
113
+ constructor(scopeDirectory, loginAid = '', gateway = '') {
114
+ this.scopeDirectory = scopeDirectory;
115
+ this.loginAid = loginAid;
116
+ if (scopeDirectory)
117
+ this.blobDirectory = join(scopeDirectory, 'blobs');
118
+ this.namespace = createHash('sha256').update(`${loginAid.trim().toLowerCase()}\0${gateway.trim()}`).digest('hex');
119
+ }
120
+ key(method, params) {
121
+ return `${this.namespace}:${storageCacheKey(method, params, this.loginAid)}`;
122
+ }
123
+ database(operation) {
124
+ if (!this.scopeDirectory || this.persistenceUnavailable)
125
+ return undefined;
126
+ let db;
127
+ try {
128
+ if (!this.initialized)
129
+ mkdirSync(this.scopeDirectory, { recursive: true });
130
+ db = new DatabaseSync(join(this.scopeDirectory, 'index.sqlite3'));
131
+ db.exec('PRAGMA busy_timeout = 5000');
132
+ db.exec('PRAGMA synchronous = NORMAL');
133
+ if (!this.initialized) {
134
+ db.exec('PRAGMA journal_mode = WAL');
135
+ db.exec('CREATE TABLE IF NOT EXISTS cache_entries(cache_key TEXT PRIMARY KEY,value_json TEXT NOT NULL,validated_at_ms INTEGER NOT NULL,expires_at_ms INTEGER NOT NULL)');
136
+ db.exec('CREATE TABLE IF NOT EXISTS cache_blobs(sha256 TEXT PRIMARY KEY,size_bytes INTEGER NOT NULL,last_access_ms INTEGER NOT NULL)');
137
+ this.initialized = true;
138
+ }
139
+ return operation(db);
140
+ }
141
+ catch {
142
+ this.persistenceUnavailable = true;
143
+ return undefined;
144
+ }
145
+ finally {
146
+ db?.close();
147
+ }
148
+ }
149
+ clearMemory() {
150
+ this.epoch += 1;
151
+ this.metadataEntries.clear();
152
+ this.bodyEntries.clear();
153
+ this.memoryOrder.clear();
154
+ this.hydrated.clear();
155
+ this.memoryBytes = 0;
156
+ this.metadataBytes = 0;
157
+ this.pending.clear();
158
+ this.activeRequests.clear();
159
+ }
160
+ invalidate() {
161
+ this.clearMemory();
162
+ this.database((db) => db.prepare('DELETE FROM cache_entries WHERE cache_key >= ? AND cache_key < ?').run(`${this.namespace}:`, `${this.namespace};`));
163
+ }
164
+ async rpc(method, params, noCache, loader) {
165
+ const key = this.key(method, params);
166
+ if ((method !== 'storage.get_object' && method !== 'storage.get_by_share') || isRangeRequest(params) || params.if_sha256 !== undefined || this.bodyContext.getStore()?.has(key)) {
167
+ return this.metadata(method, params, noCache, () => loader(params));
168
+ }
169
+ let original;
170
+ const body = await this.body(method, params, noCache, async (ifSha256) => {
171
+ original = await loader({ ...params, ...(ifSha256 ? { if_sha256: ifSha256 } : {}) });
172
+ return {
173
+ data: original.not_modified ? undefined : new Uint8Array(Buffer.from(String(original.content ?? original.content_base64 ?? ''), 'base64')),
174
+ sha256: String(original.sha256 ?? ''), metadata: original, notModified: original.not_modified === true,
175
+ cacheable: original.not_modified === true || original.download_url == null || original.content != null || original.content_base64 != null,
176
+ };
177
+ });
178
+ if (original && original.not_modified !== true)
179
+ return original;
180
+ if (body.rawResponse)
181
+ return body.rawResponse;
182
+ return { ...body.metadata, sha256: body.sha256, content: Buffer.from(body.data).toString('base64') };
183
+ }
184
+ async body(method, params, noCache, loader) {
185
+ const authoritative = params.consistency === 'authoritative' || hasExplicitSignature(params) || isRangeRequest(params);
186
+ const key = this.key(method, params);
187
+ const otherMethod = !authoritative
188
+ ? method === 'storage.get_object' ? 'storage.create_download_ticket' : method === 'storage.create_download_ticket' ? 'storage.get_object' : undefined
189
+ : undefined;
190
+ const otherKey = otherMethod ? this.key(otherMethod, params) : undefined;
191
+ const hot = this.bodyEntries.get(key);
192
+ const otherHot = otherKey ? this.bodyEntries.get(otherKey) : undefined;
193
+ const freshKey = hot && hot.expiresAt > Date.now() ? key : method === 'storage.create_download_ticket' && otherHot && otherHot.expiresAt > Date.now() ? otherKey : undefined;
194
+ if (!authoritative && !noCache && freshKey) {
195
+ const fresh = this.bodyEntries.get(freshKey);
196
+ this.bodyEntries.delete(freshKey);
197
+ this.bodyEntries.set(freshKey, fresh);
198
+ this.touchMemory(`body:${freshKey}`);
199
+ return structuredClone(fresh.value);
200
+ }
201
+ const epoch = this.epoch;
202
+ const pendingKey = `body:${epoch}:${noCache ? 'fresh' : 'normal'}:${key}`;
203
+ const pending = authoritative ? undefined : this.pending.get(pendingKey);
204
+ if (pending) {
205
+ const shared = await pending;
206
+ if (epoch === this.epoch && shared.expiresAt > Date.now())
207
+ return structuredClone(shared.value);
208
+ if (this.pending.get(pendingKey) === pending)
209
+ this.pending.delete(pendingKey);
210
+ return this.body(method, params, noCache, loader);
211
+ }
212
+ const requestKey = `${authoritative ? 'uncached' : 'body'}:${method === 'storage.create_download_ticket' && otherKey ? otherKey : key}`;
213
+ let sequence;
214
+ const request = (async () => {
215
+ const hydrationSequence = this.requestSequence;
216
+ const hydrationCurrent = () => epoch === this.epoch && hydrationSequence === this.requestSequence;
217
+ const restored = authoritative || this.bodyEntries.has(key) || this.hydrated.has(`body:${key}`) ? undefined : await this.hydrateBody(key, hydrationCurrent);
218
+ const otherRestored = !otherKey || this.bodyEntries.has(otherKey) || this.hydrated.has(`body:${otherKey}`) ? undefined : await this.hydrateBody(otherKey, hydrationCurrent);
219
+ const own = authoritative ? undefined : this.bodyEntries.get(key) ?? (hydrationCurrent() ? restored : undefined);
220
+ const other = otherKey ? this.bodyEntries.get(otherKey) ?? (hydrationCurrent() ? otherRestored : undefined) : undefined;
221
+ const cached = own && own.expiresAt > Date.now() ? own : other && other.expiresAt > Date.now() ? other : own ?? other;
222
+ if (!noCache && cached && cached.expiresAt > Date.now() && (cached === own || method === 'storage.create_download_ticket'))
223
+ return { value: cached.value, expiresAt: cached.expiresAt };
224
+ if (epoch === this.epoch)
225
+ sequence = this.beginRequest(requestKey);
226
+ const startedAt = Date.now();
227
+ const loaded = await this.bodyContext.run(new Set([...(this.bodyContext.getStore() ?? []), key]), () => loader(cached?.value.sha256));
228
+ const data = loaded.notModified ? cached?.value.data : loaded.data;
229
+ if (!data)
230
+ throw new StorageError('not_modified response requires verified cached content', 'ESTORAGE');
231
+ const hint = loaded.metadata.read_cache;
232
+ const sha256 = loaded.sha256.toLowerCase();
233
+ const validMaxAge = hint?.max_age_ms === undefined || typeof hint.max_age_ms === 'number' && Number.isFinite(hint.max_age_ms) && hint.max_age_ms > 0;
234
+ const safe = loaded.cacheable !== false && /^[0-9a-f]{64}$/.test(sha256) && (hint == null ? data.byteLength <= 65_536 : hint.inline === true && hint.reusable === true && validMaxAge);
235
+ if ((loaded.notModified || loaded.cacheable !== false && (safe || method === 'storage.get_object' || method === 'storage.get_by_share') && /^[0-9a-f]{64}$/.test(sha256)) && (!/^[0-9a-f]{64}$/.test(sha256) || (!loaded.notModified && createHash('sha256').update(data).digest('hex') !== sha256) || (loaded.notModified && cached?.value.sha256 !== sha256))) {
236
+ throw new StorageError('cached content hash verification failed', 'EHASH');
237
+ }
238
+ const value = { data: data.slice(), sha256, metadata: bodyMetadata(method, loaded.metadata) };
239
+ if (!safe && !loaded.notModified)
240
+ value.rawResponse = structuredClone(loaded.metadata);
241
+ const maxAge = hint?.max_age_ms === undefined ? METADATA_TTL_MS : validMaxAge ? Math.min(METADATA_TTL_MS, hint.max_age_ms) : 0;
242
+ const expiresAt = safe && maxAge > 0 ? startedAt + maxAge : 0;
243
+ if (!authoritative && sequence !== undefined && epoch === this.epoch && this.isCurrentRequest(requestKey, sequence)) {
244
+ this.requestSequence += 1;
245
+ this.detachBodyRequests(key, epoch);
246
+ this.deleteBody(key);
247
+ if (otherKey) {
248
+ const expiresAt = safe && maxAge > 0 && other?.value.sha256 === sha256 ? startedAt + maxAge : 0;
249
+ this.expireBodies([otherKey], epoch, expiresAt);
250
+ }
251
+ if (safe && maxAge > 0) {
252
+ this.rememberBody(key, { value, expiresAt });
253
+ await this.persistBody(key, value, expiresAt, epoch, sequence, requestKey);
254
+ }
255
+ else {
256
+ this.deleteEntry(key);
257
+ }
258
+ }
259
+ return { value, expiresAt };
260
+ })();
261
+ if (!authoritative)
262
+ this.pending.set(pendingKey, request);
263
+ try {
264
+ return structuredClone((await request).value);
265
+ }
266
+ catch (error) {
267
+ if (!authoritative && sequence !== undefined && epoch === this.epoch && this.isCurrentRequest(requestKey, sequence)) {
268
+ this.requestSequence += 1;
269
+ this.expireBodies(otherKey ? [key, otherKey] : [key], epoch, 0);
270
+ }
271
+ throw error;
272
+ }
273
+ finally {
274
+ if (this.pending.get(pendingKey) === request)
275
+ this.pending.delete(pendingKey);
276
+ if (sequence !== undefined)
277
+ this.finishRequest(requestKey, epoch);
278
+ }
279
+ }
280
+ async metadata(method, params, noCache, loader) {
281
+ if (!metadataMethods.has(method) || params.consistency === 'authoritative' || hasExplicitSignature(params))
282
+ return loader();
283
+ const key = this.key(method, params);
284
+ if (this.metadataContext.getStore()?.has(key))
285
+ return loader();
286
+ const epoch = this.epoch;
287
+ if (!this.metadataEntries.has(key))
288
+ this.hydrateMetadata(key);
289
+ const cached = this.metadataEntries.get(key);
290
+ if (!noCache && cached && cached.expiresAt > Date.now()) {
291
+ this.metadataEntries.delete(key);
292
+ this.metadataEntries.set(key, cached);
293
+ this.touchMemory(`meta:${key}`);
294
+ return structuredClone(cached.value);
295
+ }
296
+ const pendingKey = `meta:${epoch}:${noCache ? 'fresh' : 'normal'}:${key}`;
297
+ const pending = this.pending.get(pendingKey);
298
+ if (pending) {
299
+ const shared = await pending;
300
+ if (epoch === this.epoch && shared.expiresAt > Date.now())
301
+ return structuredClone(shared.value);
302
+ if (this.pending.get(pendingKey) === pending)
303
+ this.pending.delete(pendingKey);
304
+ return this.metadata(method, params, noCache, loader);
305
+ }
306
+ const requestKey = `meta:${key}`;
307
+ const sequence = this.beginRequest(requestKey);
308
+ const request = (async () => {
309
+ const startedAt = Date.now();
310
+ const value = await this.metadataContext.run(new Set([...(this.metadataContext.getStore() ?? []), key]), loader);
311
+ const reusable = value && typeof value === 'object' && this.isReusable(value.read_cache);
312
+ const hint = reusable ? value.read_cache : undefined;
313
+ const expiresAt = reusable ? startedAt + (hint ? Math.min(METADATA_TTL_MS, Number(hint.max_age_ms === undefined ? METADATA_TTL_MS : hint.max_age_ms)) : METADATA_TTL_MS) : 0;
314
+ if (epoch === this.epoch && this.isCurrentRequest(requestKey, sequence)) {
315
+ this.detachMetadataRequests(key, epoch);
316
+ if (reusable) {
317
+ this.rememberMetadata(key, value, expiresAt);
318
+ this.persistEntry(key, { kind: 'metadata', value }, expiresAt);
319
+ this.trimDiskMetadata();
320
+ }
321
+ else {
322
+ this.deleteMetadata(key);
323
+ this.deleteEntry(key);
324
+ }
325
+ }
326
+ return { value, expiresAt };
327
+ })();
328
+ this.pending.set(pendingKey, request);
329
+ try {
330
+ return structuredClone((await request).value);
331
+ }
332
+ catch (error) {
333
+ if (epoch === this.epoch && this.isCurrentRequest(requestKey, sequence)) {
334
+ this.detachMetadataRequests(key, epoch);
335
+ this.deleteMetadata(key);
336
+ this.deleteEntry(key);
337
+ }
338
+ throw error;
339
+ }
340
+ finally {
341
+ if (this.pending.get(pendingKey) === request)
342
+ this.pending.delete(pendingKey);
343
+ this.finishRequest(requestKey, epoch);
344
+ }
345
+ }
346
+ isReusable(value) {
347
+ if (value == null)
348
+ return true;
349
+ if (!value || typeof value !== 'object')
350
+ return false;
351
+ const policy = value;
352
+ return policy.reusable === true && (policy.max_age_ms === undefined || typeof policy.max_age_ms === 'number' && Number.isFinite(policy.max_age_ms) && policy.max_age_ms > 0);
353
+ }
354
+ beginRequest(key) {
355
+ const sequence = ++this.requestSequence;
356
+ this.activeRequests.set(key, { sequence, count: (this.activeRequests.get(key)?.count ?? 0) + 1, epoch: this.epoch });
357
+ return sequence;
358
+ }
359
+ isCurrentRequest(key, sequence) {
360
+ return this.activeRequests.get(key)?.sequence === sequence;
361
+ }
362
+ finishRequest(key, epoch) {
363
+ const state = this.activeRequests.get(key);
364
+ if (state?.epoch === epoch && --state.count === 0)
365
+ this.activeRequests.delete(key);
366
+ }
367
+ expireBodies(keys, epoch, expiresAt) {
368
+ for (const key of keys) {
369
+ const entry = this.bodyEntries.get(key);
370
+ if (entry)
371
+ entry.expiresAt = Math.min(entry.expiresAt, expiresAt);
372
+ this.detachBodyRequests(key, epoch);
373
+ }
374
+ this.database((db) => {
375
+ const update = db.prepare('UPDATE cache_entries SET expires_at_ms = MIN(expires_at_ms, ?) WHERE cache_key = ?');
376
+ for (const key of keys)
377
+ update.run(expiresAt, key);
378
+ });
379
+ }
380
+ detachBodyRequests(key, epoch) {
381
+ this.pending.delete(`body:${epoch}:normal:${key}`);
382
+ this.pending.delete(`body:${epoch}:fresh:${key}`);
383
+ }
384
+ detachMetadataRequests(key, epoch) {
385
+ this.pending.delete(`meta:${epoch}:normal:${key}`);
386
+ this.pending.delete(`meta:${epoch}:fresh:${key}`);
387
+ }
388
+ entry(key) {
389
+ return this.database((db) => db.prepare('SELECT value_json,expires_at_ms FROM cache_entries WHERE cache_key = ?').get(key));
390
+ }
391
+ deleteEntry(key) {
392
+ this.database((db) => db.prepare('DELETE FROM cache_entries WHERE cache_key = ?').run(key));
393
+ }
394
+ hydrateMetadata(key) {
395
+ if (this.hydrated.has(`meta:${key}`))
396
+ return;
397
+ this.markHydrated(`meta:${key}`);
398
+ const row = this.entry(key);
399
+ if (!row)
400
+ return;
401
+ try {
402
+ const parsed = JSON.parse(row.value_json);
403
+ if (row.expires_at_ms > Date.now() && parsed.kind === 'metadata' && parsed.value && this.isReusable(parsed.value.read_cache)) {
404
+ this.rememberMetadata(key, parsed.value, Number(row.expires_at_ms));
405
+ }
406
+ else
407
+ this.deleteEntry(key);
408
+ }
409
+ catch {
410
+ this.deleteEntry(key);
411
+ }
412
+ }
413
+ async hydrateBody(key, isCurrent) {
414
+ if (this.hydrated.has(`body:${key}`))
415
+ return undefined;
416
+ this.markHydrated(`body:${key}`);
417
+ if (!this.blobDirectory)
418
+ return undefined;
419
+ const row = this.entry(key);
420
+ if (!row)
421
+ return undefined;
422
+ let blobHash;
423
+ try {
424
+ const parsed = JSON.parse(row.value_json);
425
+ const hint = parsed.metadata?.read_cache;
426
+ if (parsed.kind !== 'body' || !parsed.metadata || !parsed.sha256 || !/^[0-9a-f]{64}$/.test(parsed.sha256) || hint != null && (hint.inline !== true || !this.isReusable(hint)))
427
+ throw new Error('invalid cached entry');
428
+ blobHash = parsed.sha256;
429
+ const data = await readFile(join(this.blobDirectory, parsed.sha256.slice(0, 2), parsed.sha256));
430
+ if (hint == null && data.byteLength > 65_536 || createHash('sha256').update(data).digest('hex') !== parsed.sha256)
431
+ throw new Error('invalid cached blob');
432
+ const entry = { value: { data: new Uint8Array(data), sha256: parsed.sha256, metadata: parsed.metadata }, expiresAt: Number(row.expires_at_ms) };
433
+ if (!isCurrent())
434
+ return undefined;
435
+ this.rememberBody(key, entry);
436
+ this.database((db) => db.prepare('UPDATE cache_blobs SET last_access_ms = ? WHERE sha256 = ?').run(Date.now(), parsed.sha256));
437
+ return entry;
438
+ }
439
+ catch {
440
+ if (isCurrent()) {
441
+ if (blobHash) {
442
+ this.database((db) => {
443
+ db.exec('BEGIN IMMEDIATE');
444
+ try {
445
+ const blobPath = join(this.blobDirectory, blobHash.slice(0, 2), blobHash);
446
+ let repaired = false;
447
+ try {
448
+ repaired = createHash('sha256').update(readFileSync(blobPath)).digest('hex') === blobHash;
449
+ }
450
+ catch { }
451
+ if (!repaired) {
452
+ rmSync(blobPath, { force: true });
453
+ db.prepare('DELETE FROM cache_entries WHERE json_extract(value_json,\'$.sha256\') = ?').run(blobHash);
454
+ db.prepare('DELETE FROM cache_blobs WHERE sha256 = ?').run(blobHash);
455
+ }
456
+ db.exec('COMMIT');
457
+ }
458
+ catch (error) {
459
+ db.exec('ROLLBACK');
460
+ throw error;
461
+ }
462
+ });
463
+ }
464
+ else
465
+ this.deleteEntry(key);
466
+ }
467
+ return undefined;
468
+ }
469
+ }
470
+ rememberBody(key, entry) {
471
+ this.deleteBody(key);
472
+ entry.metadataSize = Buffer.byteLength(JSON.stringify(entry.value.metadata), 'utf8');
473
+ if (entry.value.data.byteLength > MEMORY_LIMIT_BYTES || entry.metadataSize > METADATA_LIMIT_BYTES)
474
+ return;
475
+ this.bodyEntries.set(key, entry);
476
+ this.memoryBytes += entry.value.data.byteLength;
477
+ this.metadataBytes += entry.metadataSize;
478
+ this.touchMemory(`body:${key}`);
479
+ this.trimMemoryMetadata();
480
+ while (this.memoryBytes > MEMORY_LIMIT_BYTES) {
481
+ const oldest = this.bodyEntries.keys().next().value;
482
+ this.deleteBody(oldest);
483
+ this.hydrated.delete(`body:${oldest}`);
484
+ }
485
+ }
486
+ deleteBody(key) {
487
+ const prior = this.bodyEntries.get(key);
488
+ if (prior) {
489
+ this.memoryBytes -= prior.value.data.byteLength;
490
+ this.metadataBytes -= prior.metadataSize ?? 0;
491
+ }
492
+ this.bodyEntries.delete(key);
493
+ this.memoryOrder.delete(`body:${key}`);
494
+ }
495
+ rememberMetadata(key, value, expiresAt) {
496
+ this.deleteMetadata(key);
497
+ const size = Buffer.byteLength(JSON.stringify(value), 'utf8');
498
+ if (size <= METADATA_LIMIT_BYTES) {
499
+ this.metadataEntries.set(key, { value: structuredClone(value), expiresAt, size });
500
+ this.metadataBytes += size;
501
+ this.touchMemory(`meta:${key}`);
502
+ }
503
+ this.trimMemoryMetadata();
504
+ }
505
+ deleteMetadata(key) {
506
+ const prior = this.metadataEntries.get(key);
507
+ if (prior)
508
+ this.metadataBytes -= prior.size;
509
+ this.metadataEntries.delete(key);
510
+ this.memoryOrder.delete(`meta:${key}`);
511
+ }
512
+ touchMemory(key) {
513
+ this.memoryOrder.delete(key);
514
+ this.memoryOrder.add(key);
515
+ }
516
+ markHydrated(key) {
517
+ this.hydrated.add(key);
518
+ if (this.hydrated.size > MEMORY_LIMIT_COUNT)
519
+ this.hydrated.delete(this.hydrated.values().next().value);
520
+ }
521
+ trimMemoryMetadata() {
522
+ while (this.memoryOrder.size > MEMORY_LIMIT_COUNT || this.metadataBytes > METADATA_LIMIT_BYTES) {
523
+ const oldest = this.memoryOrder.values().next().value;
524
+ if (oldest.startsWith('body:'))
525
+ this.deleteBody(oldest.slice(5));
526
+ else
527
+ this.deleteMetadata(oldest.slice(5));
528
+ this.hydrated.delete(oldest);
529
+ }
530
+ }
531
+ persistEntry(key, value, expiresAt, database) {
532
+ const persist = (db) => db.prepare('INSERT INTO cache_entries(cache_key,value_json,validated_at_ms,expires_at_ms) VALUES(?,?,?,?) ON CONFLICT(cache_key) DO UPDATE SET value_json=excluded.value_json,validated_at_ms=excluded.validated_at_ms,expires_at_ms=excluded.expires_at_ms').run(key, JSON.stringify(value), Date.now(), expiresAt);
533
+ if (database)
534
+ persist(database);
535
+ else
536
+ this.database(persist);
537
+ }
538
+ async persistBody(key, value, expiresAt, epoch, sequence, requestKey = `body:${key}`) {
539
+ if (!this.blobDirectory || this.persistenceUnavailable)
540
+ return;
541
+ const blobDir = join(this.blobDirectory, value.sha256.slice(0, 2));
542
+ const blobPath = join(blobDir, value.sha256);
543
+ let tempPath;
544
+ try {
545
+ const known = this.database((db) => Boolean(db.prepare('SELECT 1 FROM cache_blobs WHERE sha256 = ?').get(value.sha256)));
546
+ if (this.persistenceUnavailable)
547
+ return;
548
+ if (!known) {
549
+ await mkdir(blobDir, { recursive: true });
550
+ tempPath = `${blobPath}.${process.pid}.${randomUUID()}.tmp`;
551
+ await writeFile(tempPath, value.data, { flag: 'wx' });
552
+ }
553
+ this.database((db) => {
554
+ db.exec('BEGIN IMMEDIATE');
555
+ let published = false;
556
+ try {
557
+ if (!db.prepare('SELECT 1 FROM cache_blobs WHERE sha256 = ?').get(value.sha256)) {
558
+ if (tempPath)
559
+ renameSync(tempPath, blobPath);
560
+ else {
561
+ mkdirSync(blobDir, { recursive: true });
562
+ writeFileSync(blobPath, value.data);
563
+ }
564
+ published = true;
565
+ }
566
+ db.prepare('INSERT INTO cache_blobs(sha256,size_bytes,last_access_ms) VALUES(?,?,?) ON CONFLICT(sha256) DO UPDATE SET last_access_ms=excluded.last_access_ms').run(value.sha256, value.data.byteLength, Date.now());
567
+ if (epoch === this.epoch && this.isCurrentRequest(requestKey, sequence))
568
+ this.persistEntry(key, { kind: 'body', sha256: value.sha256, metadata: value.metadata }, expiresAt, db);
569
+ this.trimDiskMetadata(db);
570
+ this.evictDisk(db);
571
+ db.exec('COMMIT');
572
+ }
573
+ catch (error) {
574
+ if (published)
575
+ rmSync(blobPath, { force: true });
576
+ db.exec('ROLLBACK');
577
+ throw error;
578
+ }
579
+ });
580
+ }
581
+ catch {
582
+ if (epoch === this.epoch && this.isCurrentRequest(requestKey, sequence))
583
+ this.deleteEntry(key);
584
+ }
585
+ finally {
586
+ if (tempPath)
587
+ await unlink(tempPath).catch(() => undefined);
588
+ }
589
+ }
590
+ evictDisk(db) {
591
+ const totals = db.prepare('SELECT COUNT(*) AS count,COALESCE(SUM(size_bytes),0) AS total FROM cache_blobs').get();
592
+ let count = Number(totals.count);
593
+ let total = Number(totals.total);
594
+ while (count > DISK_LIMIT_COUNT || total > DISK_LIMIT_BYTES) {
595
+ const row = db.prepare('SELECT sha256,size_bytes FROM cache_blobs ORDER BY last_access_ms ASC LIMIT 1').get();
596
+ if (!row)
597
+ break;
598
+ rmSync(join(this.blobDirectory, row.sha256.slice(0, 2), row.sha256), { force: true });
599
+ db.prepare('DELETE FROM cache_entries WHERE json_extract(value_json,\'$.sha256\') = ?').run(row.sha256);
600
+ db.prepare('DELETE FROM cache_blobs WHERE sha256 = ?').run(row.sha256);
601
+ count -= 1;
602
+ total -= Number(row.size_bytes);
603
+ }
604
+ }
605
+ trimDiskMetadata(database) {
606
+ const trim = (db) => {
607
+ const totals = db.prepare('SELECT COUNT(*) AS count,COALESCE(SUM(length(CAST(value_json AS BLOB))),0) AS size FROM cache_entries').get();
608
+ let count = Number(totals.count);
609
+ let size = Number(totals.size);
610
+ while (count > DISK_LIMIT_COUNT || size > METADATA_LIMIT_BYTES) {
611
+ const oldest = db.prepare('SELECT cache_key,length(CAST(value_json AS BLOB)) AS size FROM cache_entries ORDER BY validated_at_ms ASC,cache_key ASC LIMIT 1').get();
612
+ if (!oldest)
613
+ break;
614
+ db.prepare('DELETE FROM cache_entries WHERE cache_key = ?').run(oldest.cache_key);
615
+ count -= 1;
616
+ size -= Number(oldest.size);
617
+ }
618
+ };
619
+ if (database)
620
+ trim(database);
621
+ else
622
+ this.database(trim);
623
+ }
624
+ }
625
+ //# sourceMappingURL=read-cache.js.map