@ariestools/aries-dapp-core 0.1.7 → 0.1.8

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/README.md +253 -32
  2. package/dist/bin/dappServer.mjs +614 -100
  3. package/dist/node/DappActor.d.ts +28 -7
  4. package/dist/node/DappActor.d.ts.map +1 -1
  5. package/dist/node/actor/AbstractActor.d.ts +13 -8
  6. package/dist/node/actor/AbstractActor.d.ts.map +1 -1
  7. package/dist/node/actor/types.d.ts +7 -0
  8. package/dist/node/actor/types.d.ts.map +1 -1
  9. package/dist/node/bin/dappServer.d.ts +8 -4
  10. package/dist/node/bin/dappServer.d.ts.map +1 -1
  11. package/dist/node/boot/bootDappActors.d.ts +12 -1
  12. package/dist/node/boot/bootDappActors.d.ts.map +1 -1
  13. package/dist/node/consumer/api.d.ts +56 -0
  14. package/dist/node/consumer/api.d.ts.map +1 -0
  15. package/dist/node/consumer/hash.d.ts +6 -0
  16. package/dist/node/consumer/hash.d.ts.map +1 -0
  17. package/dist/node/consumer/parse.d.ts +18 -0
  18. package/dist/node/consumer/parse.d.ts.map +1 -0
  19. package/dist/node/consumer.d.ts +9 -0
  20. package/dist/node/consumer.d.ts.map +1 -0
  21. package/dist/node/consumer.mjs +330 -0
  22. package/dist/node/consumer.mjs.map +7 -0
  23. package/dist/node/examples/productionComposition.d.ts +114 -0
  24. package/dist/node/examples/productionComposition.d.ts.map +1 -0
  25. package/dist/node/index.d.ts +25 -3
  26. package/dist/node/index.d.ts.map +1 -1
  27. package/dist/node/index.mjs +1730 -68
  28. package/dist/node/index.mjs.map +4 -4
  29. package/dist/node/locator/createDappLocator.d.ts +32 -5
  30. package/dist/node/locator/createDappLocator.d.ts.map +1 -1
  31. package/dist/node/providers/DappBucketStore.d.ts +26 -21
  32. package/dist/node/providers/DappBucketStore.d.ts.map +1 -1
  33. package/dist/node/providers/capabilities.d.ts +49 -0
  34. package/dist/node/providers/capabilities.d.ts.map +1 -0
  35. package/dist/node/providers/errors.d.ts +11 -0
  36. package/dist/node/providers/errors.d.ts.map +1 -0
  37. package/dist/node/providers/hashBytes.d.ts +3 -0
  38. package/dist/node/providers/hashBytes.d.ts.map +1 -0
  39. package/dist/node/providers/joinKey.d.ts +11 -0
  40. package/dist/node/providers/joinKey.d.ts.map +1 -0
  41. package/dist/node/providers/memoryBucketStore.d.ts +14 -0
  42. package/dist/node/providers/memoryBucketStore.d.ts.map +1 -0
  43. package/dist/node/providers/types.d.ts +99 -0
  44. package/dist/node/providers/types.d.ts.map +1 -0
  45. package/dist/node/publication/canonicalHead.d.ts +7 -0
  46. package/dist/node/publication/canonicalHead.d.ts.map +1 -0
  47. package/dist/node/publication/commitHead.d.ts +9 -0
  48. package/dist/node/publication/commitHead.d.ts.map +1 -0
  49. package/dist/node/publication/constants.d.ts +27 -0
  50. package/dist/node/publication/constants.d.ts.map +1 -0
  51. package/dist/node/publication/gc.d.ts +42 -0
  52. package/dist/node/publication/gc.d.ts.map +1 -0
  53. package/dist/node/publication/incremental.d.ts +83 -0
  54. package/dist/node/publication/incremental.d.ts.map +1 -0
  55. package/dist/node/publication/publishGeneration.d.ts +18 -0
  56. package/dist/node/publication/publishGeneration.d.ts.map +1 -0
  57. package/dist/node/publication/putCreateOnly.d.ts +11 -0
  58. package/dist/node/publication/putCreateOnly.d.ts.map +1 -0
  59. package/dist/node/publication/releases.d.ts +49 -0
  60. package/dist/node/publication/releases.d.ts.map +1 -0
  61. package/dist/node/publication/safety.d.ts +55 -0
  62. package/dist/node/publication/safety.d.ts.map +1 -0
  63. package/dist/node/publication/shard.d.ts +7 -0
  64. package/dist/node/publication/shard.d.ts.map +1 -0
  65. package/dist/node/publication/status.d.ts +27 -0
  66. package/dist/node/publication/status.d.ts.map +1 -0
  67. package/dist/node/publication/statusPatch.d.ts +10 -0
  68. package/dist/node/publication/statusPatch.d.ts.map +1 -0
  69. package/dist/node/publication/types.d.ts +138 -0
  70. package/dist/node/publication/types.d.ts.map +1 -0
  71. package/dist/node/publication/writeGenerationObjects.d.ts +5 -0
  72. package/dist/node/publication/writeGenerationObjects.d.ts.map +1 -0
  73. package/dist/node/reducer/DappReducer.d.ts +37 -14
  74. package/dist/node/reducer/DappReducer.d.ts.map +1 -1
  75. package/package.json +12 -5
@@ -24,6 +24,8 @@ function createDeferred() {
24
24
  resolve = res;
25
25
  reject = rej;
26
26
  });
27
+ void promise.catch(() => {
28
+ });
27
29
  return {
28
30
  promise,
29
31
  reject,
@@ -33,6 +35,7 @@ function createDeferred() {
33
35
  var AbstractActor = class extends AbstractCreatable {
34
36
  _intervals = /* @__PURE__ */ new Map();
35
37
  _timeouts = /* @__PURE__ */ new Map();
38
+ _abortController = new AbortController();
36
39
  _idLogger;
37
40
  _inFlight = /* @__PURE__ */ new Map();
38
41
  _readyDeferred = createDeferred();
@@ -57,17 +60,25 @@ var AbstractActor = class extends AbstractCreatable {
57
60
  get locator() {
58
61
  return this.params.locator;
59
62
  }
63
+ /** Abort signal cancelled when the actor stops (or is replaced on restart). */
64
+ get signal() {
65
+ return this._abortController.signal;
66
+ }
60
67
  static async paramsHandler(params) {
61
68
  const inParams = params ?? {};
62
69
  const baseParams = await super.paramsHandler({ ...inParams, name: inParams.name ?? "UnknownActor" });
63
70
  const locator = assertEx(inParams.locator, () => `params.locator is required for actor ${String(baseParams.name)}.`);
64
71
  return { ...baseParams, locator };
65
72
  }
73
+ /** Override to prove the actor can do useful work. Default: no-op. */
74
+ async readyHandler() {
75
+ }
66
76
  /**
67
- * Register a recurring task. The interval only fires while the actor is
68
- * `started`, and a run is skipped if the previous one is still in flight, so
69
- * a slow pass can never stack on top of itself. Must be called from
70
- * `startHandler` (guards on `starting` status), mirroring the xl1 actor.
77
+ * Register a recurring task. The first invocation fires after `dueTimeMs`
78
+ * (the documented first-run delay); subsequent invocations fire every
79
+ * `periodMs` while the actor is `started`. A run is skipped if the previous
80
+ * one is still in flight, so a slow pass can never stack on top of itself.
81
+ * Must be called from `startHandler` (guards on `starting` status).
71
82
  */
72
83
  registerTimer(timerName, callback, dueTimeMs, periodMs) {
73
84
  if (this.status !== "starting") {
@@ -75,7 +86,7 @@ var AbstractActor = class extends AbstractCreatable {
75
86
  return;
76
87
  }
77
88
  const tick = () => {
78
- if (this.status !== "started" || !this._intervals.has(timerName)) return;
89
+ if (this.status !== "started") return;
79
90
  if (this._inFlight.has(timerName)) {
80
91
  this.logger.warn(`Skipping timer '${this.name}:${timerName}' because the previous run is still in flight.`);
81
92
  return;
@@ -99,13 +110,15 @@ var AbstractActor = class extends AbstractCreatable {
99
110
  }));
100
111
  };
101
112
  const timeoutId = setTimeout(() => {
113
+ if (this.status !== "started") return;
102
114
  this._intervals.set(timerName, setInterval(tick, periodMs));
115
+ tick();
103
116
  }, dueTimeMs);
104
117
  this._timeouts.set(timerName, timeoutId);
105
118
  }
106
119
  /**
107
- * Run the warm-pass once. Not driven automatically here (no orchestrator);
108
- * a caller invokes it after `start()` if it wants `whenReady()` to resolve.
120
+ * Run the warm-pass once. The standard boot path invokes this after
121
+ * `start()` so readiness either resolves or rejects.
109
122
  */
110
123
  async runReadyHandler() {
111
124
  if (this._readyState !== "pending") return;
@@ -121,60 +134,310 @@ var AbstractActor = class extends AbstractCreatable {
121
134
  throw err;
122
135
  }
123
136
  }
137
+ async startHandler() {
138
+ if (this._abortController.signal.aborted) {
139
+ this._abortController = new AbortController();
140
+ }
141
+ await super.startHandler();
142
+ }
124
143
  async stopHandler() {
125
144
  await super.stopHandler();
145
+ this._abortController.abort();
126
146
  for (const timeoutRef of this._timeouts.values()) clearTimeout(timeoutRef);
127
147
  this._timeouts.clear();
128
148
  for (const intervalRef of this._intervals.values()) clearInterval(intervalRef);
129
149
  this._intervals.clear();
130
- await Promise.allSettled(this._inFlight.values());
150
+ const inFlight = [...this._inFlight.values()];
131
151
  this._inFlight.clear();
152
+ const timeoutMs = this.params.shutdownTimeoutMs;
153
+ if (timeoutMs === void 0) {
154
+ await Promise.allSettled(inFlight);
155
+ return;
156
+ }
157
+ await Promise.race([
158
+ Promise.allSettled(inFlight),
159
+ new Promise((resolve) => {
160
+ setTimeout(resolve, timeoutMs);
161
+ })
162
+ ]);
132
163
  }
133
164
  async whenReady() {
134
165
  await this._readyDeferred.promise;
135
166
  }
136
- /** Override to prove the actor can do useful work. Default: no-op. */
137
- async readyHandler() {
138
- }
139
167
  };
140
168
 
141
169
  // src/DappActor.ts
142
170
  import { creatable } from "@ariestools/sdk";
143
171
 
172
+ // src/providers/capabilities.ts
173
+ var ReadOnlyStoreError = class extends Error {
174
+ name = "ReadOnlyStoreError";
175
+ constructor(role, operation = "put") {
176
+ super(`Store role "${role}" is read-only; ${operation} is not permitted`);
177
+ }
178
+ };
179
+ function bindReader(store) {
180
+ return {
181
+ role: store.role,
182
+ bucket: store.bucket,
183
+ prefix: store.prefix,
184
+ ...store.publicBaseUrl !== void 0 && { publicBaseUrl: store.publicBaseUrl },
185
+ destroy: () => store.destroy(),
186
+ get: (key) => store.get(key),
187
+ getWithMeta: (key) => store.getWithMeta(key),
188
+ list: (listPrefix) => store.list(listPrefix),
189
+ listAsync: (listPrefix) => store.listAsync(listPrefix),
190
+ listPage: (options) => store.listPage(options),
191
+ publicUrl: (key) => store.publicUrl(key),
192
+ resolveKey: (key) => store.resolveKey(key),
193
+ stat: (key) => store.stat(key)
194
+ };
195
+ }
196
+ function asReadOnly(store) {
197
+ return bindReader(store);
198
+ }
199
+ function asReadOnlyWriter(store) {
200
+ const reader = bindReader(store);
201
+ return {
202
+ ...reader,
203
+ async put() {
204
+ throw new ReadOnlyStoreError(store.role, "put");
205
+ },
206
+ async delete() {
207
+ throw new ReadOnlyStoreError(store.role, "delete");
208
+ }
209
+ };
210
+ }
211
+
144
212
  // src/providers/monikers.ts
145
213
  var DappDataStoreMoniker = "DappDataStore";
146
214
  var DappStateStoreMoniker = "DappStateStore";
147
215
  var DappIndexStoreMoniker = "DappIndexStore";
148
216
 
217
+ // src/publication/constants.ts
218
+ var HEAD_KEY = "head.json";
219
+ var STATUS_KEY = "status.json";
220
+ var PINS_KEY = "pins.json";
221
+ var GENERATIONS_PREFIX = "generations";
222
+ var CAS_PREFIX = "cas/sha256";
223
+ var MAP_ROOTS_PREFIX = "map/roots";
224
+ var MAP_SHARDS_PREFIX = "map/shards";
225
+ var RECEIPTS_PREFIX = "receipts";
226
+ var CACHE_CONTROL_IMMUTABLE = "public, max-age=31536000, immutable";
227
+ var CACHE_CONTROL_REVALIDATE = "public, max-age=0, must-revalidate";
228
+ var GENERATION_MANIFEST_SCHEMA = "network.aries.dapp.generation.manifest";
229
+ var HEAD_MANIFEST_SCHEMA = "network.aries.dapp.head";
230
+ var INDEXER_STATUS_SCHEMA = "network.aries.dapp.indexer.status";
231
+ var PINS_SCHEMA = "network.aries.dapp.pins";
232
+ var MAP_ROOT_SCHEMA = "network.aries.dapp.map.root";
233
+ var MAP_SHARD_SCHEMA = "network.aries.dapp.map.shard";
234
+ var RECEIPT_SCHEMA = "network.aries.dapp.generation.receipt";
235
+ var DEFAULT_SHARD_HEX_CHARS = 2;
236
+
237
+ // src/publication/status.ts
238
+ function encodeStatus(status) {
239
+ return `${JSON.stringify(status, null, 2)}
240
+ `;
241
+ }
242
+ async function readIndexerStatus(store, key = STATUS_KEY) {
243
+ const body = await store.get(key);
244
+ if (body === void 0) return void 0;
245
+ const parsed = JSON.parse(new TextDecoder().decode(body));
246
+ if (parsed === null || typeof parsed !== "object") {
247
+ throw new Error(`Invalid indexer status at "${key}": not an object`);
248
+ }
249
+ const record = parsed;
250
+ if (record.schema !== INDEXER_STATUS_SCHEMA) {
251
+ throw new Error(`Invalid indexer status schema at "${key}": ${String(record.schema)}`);
252
+ }
253
+ return record;
254
+ }
255
+ async function writeIndexerStatus(store, input) {
256
+ const key = input.key ?? STATUS_KEY;
257
+ const status = {
258
+ schema: INDEXER_STATUS_SCHEMA,
259
+ consecutiveFailures: input.consecutiveFailures,
260
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
261
+ ...input.floor !== void 0 && { floor: input.floor },
262
+ ...input.cursor !== void 0 && { cursor: input.cursor },
263
+ ...input.lastCompletedPosition !== void 0 && { lastCompletedPosition: input.lastCompletedPosition },
264
+ ...input.observedSourceHead !== void 0 && { observedSourceHead: input.observedSourceHead },
265
+ ...input.generation !== void 0 && { generation: input.generation },
266
+ ...input.generationRoot !== void 0 && { generationRoot: input.generationRoot },
267
+ ...input.lastSuccessAt !== void 0 && { lastSuccessAt: input.lastSuccessAt },
268
+ ...input.lastErrorAt !== void 0 && { lastErrorAt: input.lastErrorAt },
269
+ ...input.lastError !== void 0 && { lastError: input.lastError }
270
+ };
271
+ await store.put(key, encodeStatus(status), {
272
+ contentType: "application/json",
273
+ cacheControl: CACHE_CONTROL_REVALIDATE
274
+ });
275
+ return status;
276
+ }
277
+
278
+ // src/publication/statusPatch.ts
279
+ function pickDefined(source, keys) {
280
+ const out = {};
281
+ for (const key of keys) {
282
+ const value = source[key];
283
+ if (value !== void 0) out[key] = value;
284
+ }
285
+ return out;
286
+ }
287
+ var PROGRESS_KEYS = [
288
+ "floor",
289
+ "cursor",
290
+ "lastCompletedPosition",
291
+ "observedSourceHead",
292
+ "generation",
293
+ "generationRoot"
294
+ ];
295
+ function priorProgressFields(prior) {
296
+ if (prior === void 0) return {};
297
+ return {
298
+ ...pickDefined(prior, PROGRESS_KEYS),
299
+ ...prior.lastSuccessAt !== void 0 && { lastSuccessAt: prior.lastSuccessAt }
300
+ };
301
+ }
302
+ function successStatusFields(progress, prior, now) {
303
+ const fromProgress = progress === void 0 ? {} : pickDefined(progress, PROGRESS_KEYS);
304
+ const generation = progress?.generation ?? prior?.generation;
305
+ const generationRoot = progress?.generationRoot ?? prior?.generationRoot;
306
+ return {
307
+ consecutiveFailures: 0,
308
+ ...fromProgress,
309
+ ...generation !== void 0 && { generation },
310
+ ...generationRoot !== void 0 && { generationRoot },
311
+ lastSuccessAt: now,
312
+ ...prior?.lastError !== void 0 && { lastError: prior.lastError },
313
+ ...prior?.lastErrorAt !== void 0 && { lastErrorAt: prior.lastErrorAt }
314
+ };
315
+ }
316
+ function failureStatusFields(prior, consecutiveFailures, error, now) {
317
+ return {
318
+ consecutiveFailures,
319
+ ...priorProgressFields(prior),
320
+ lastError: error.message,
321
+ lastErrorAt: now
322
+ };
323
+ }
324
+
149
325
  // src/DappActor.ts
150
326
  var DEFAULT_REDUCE_INTERVAL_MS = 5e3;
151
327
  var DEFAULT_FIRST_RUN_DELAY_MS = 2e3;
328
+ var REDUCE_TIMER = "DappReduce";
329
+ function createDeferred2() {
330
+ let resolve;
331
+ let reject;
332
+ const promise = new Promise((res, rej) => {
333
+ resolve = res;
334
+ reject = rej;
335
+ });
336
+ void promise.catch(() => {
337
+ });
338
+ return {
339
+ promise,
340
+ reject,
341
+ resolve
342
+ };
343
+ }
344
+ function withDefaultGenerationRoot(result) {
345
+ if (result === void 0) return;
346
+ if (result.generation === void 0 || result.generationRoot !== void 0) return result;
347
+ return { ...result, generationRoot: HEAD_KEY };
348
+ }
152
349
  var DappActor = class extends AbstractActor {
153
350
  _data;
154
351
  _index;
155
352
  _state;
353
+ _consecutiveFailures = 0;
354
+ _firstPass = createDeferred2();
355
+ _firstPassSettled = false;
356
+ /** Consecutive reduce failures since the last success. */
357
+ get consecutiveFailures() {
358
+ return this._consecutiveFailures;
359
+ }
156
360
  async createHandler() {
157
361
  await super.createHandler();
158
- this._data = await this.locator.getInstance(DappDataStoreMoniker);
362
+ const dataWriter = await this.locator.getInstance(DappDataStoreMoniker);
363
+ this._data = asReadOnly(dataWriter);
159
364
  this._state = await this.locator.getInstance(DappStateStoreMoniker);
160
365
  this._index = await this.locator.getInstance(DappIndexStoreMoniker);
161
366
  }
367
+ async readyHandler() {
368
+ await this._firstPass.promise;
369
+ }
162
370
  async startHandler() {
163
371
  await super.startHandler();
164
372
  const interval = this.params.reduceIntervalMs ?? DEFAULT_REDUCE_INTERVAL_MS;
165
373
  const firstRunDelay = this.params.firstRunDelayMs ?? DEFAULT_FIRST_RUN_DELAY_MS;
166
- this.registerTimer("DappReduce", async () => {
374
+ this.registerTimer(REDUCE_TIMER, async () => {
167
375
  await this.runReducePass();
168
376
  }, firstRunDelay, interval);
169
- this.logger.info(`DappActor started: reducer '${this.params.reducer.name}' every ${interval}ms`);
377
+ this.logger.info(
378
+ `DappActor started: reducer '${this.params.reducer.name}' every ${interval}ms (first run in ${firstRunDelay}ms)`
379
+ );
380
+ }
381
+ async stopHandler() {
382
+ this.settleFirstPass(new Error("Actor stopped before first reduce pass completed"));
383
+ await super.stopHandler();
384
+ }
385
+ async publishStatusFromProgress(progress, error) {
386
+ if (this.params.publishStatus === false) return;
387
+ let prior;
388
+ try {
389
+ prior = await readIndexerStatus(this._state);
390
+ } catch {
391
+ prior = void 0;
392
+ }
393
+ const now = (/* @__PURE__ */ new Date()).toISOString();
394
+ const fields = error === void 0 ? successStatusFields(progress, prior, now) : failureStatusFields(prior, this._consecutiveFailures, error, now);
395
+ await writeIndexerStatus(this._state, fields);
170
396
  }
171
397
  async runReducePass() {
172
- await this.params.reducer.reduce({
173
- data: this._data,
174
- index: this._index,
175
- logger: this.logger,
176
- state: this._state
177
- });
398
+ try {
399
+ const result = await this.params.reducer.reduce({
400
+ data: this._data,
401
+ index: this._index,
402
+ logger: this.logger,
403
+ signal: this.signal,
404
+ state: this._state
405
+ });
406
+ const progress = withDefaultGenerationRoot(result === void 0 ? void 0 : result);
407
+ this._consecutiveFailures = 0;
408
+ await this.safePublishStatus(progress);
409
+ this.settleFirstPass();
410
+ } catch (error) {
411
+ const err = error instanceof Error ? error : new Error(String(error));
412
+ this._consecutiveFailures += 1;
413
+ await this.safePublishStatus(void 0, err);
414
+ this.settleFirstPass(err);
415
+ this.logger.error(
416
+ `Error in reduce pass for '${this.params.reducer.name}': ${err.message}`
417
+ );
418
+ const max = this.params.maxConsecutiveFailures;
419
+ if (max !== void 0 && this._consecutiveFailures >= max) {
420
+ this.logger.error(
421
+ `Reducer '${this.params.reducer.name}' failed ${this._consecutiveFailures} consecutive times; stopping actor`
422
+ );
423
+ setTimeout(() => {
424
+ void this.stop();
425
+ }, 0);
426
+ }
427
+ }
428
+ }
429
+ async safePublishStatus(progress, error) {
430
+ try {
431
+ await this.publishStatusFromProgress(progress, error);
432
+ } catch (statusError) {
433
+ this.logger.error(`Failed to publish indexer status: ${String(statusError)}`);
434
+ }
435
+ }
436
+ settleFirstPass(error) {
437
+ if (this._firstPassSettled) return;
438
+ this._firstPassSettled = true;
439
+ if (error === void 0) this._firstPass.resolve();
440
+ else this._firstPass.reject(error);
178
441
  }
179
442
  };
180
443
  __publicField(DappActor, "dependencies", [
@@ -189,18 +452,35 @@ DappActor = __decorateClass([
189
452
  // src/boot/bootDappActors.ts
190
453
  async function bootDappActors(locator, specs) {
191
454
  const actors = [];
192
- for (const spec of specs) {
193
- const params = {
194
- locator,
195
- name: spec.name,
196
- reducer: spec.reducer,
197
- ...spec.reduceIntervalMs !== void 0 && { reduceIntervalMs: spec.reduceIntervalMs }
198
- };
199
- const actor = await DappActor.create(params);
200
- await actor.start();
201
- actors.push(actor);
455
+ try {
456
+ for (const spec of specs) {
457
+ const params = {
458
+ locator,
459
+ name: spec.name,
460
+ reducer: spec.reducer,
461
+ ...spec.reduceIntervalMs !== void 0 && { reduceIntervalMs: spec.reduceIntervalMs },
462
+ ...spec.firstRunDelayMs !== void 0 && { firstRunDelayMs: spec.firstRunDelayMs },
463
+ ...spec.maxConsecutiveFailures !== void 0 && { maxConsecutiveFailures: spec.maxConsecutiveFailures },
464
+ ...spec.publishStatus !== void 0 && { publishStatus: spec.publishStatus },
465
+ ...spec.shutdownTimeoutMs !== void 0 && { shutdownTimeoutMs: spec.shutdownTimeoutMs }
466
+ };
467
+ const actor = await DappActor.create(params);
468
+ await actor.start();
469
+ actors.push(actor);
470
+ if (spec.runReady !== false) {
471
+ await actor.runReadyHandler();
472
+ }
473
+ }
474
+ return actors;
475
+ } catch (error) {
476
+ for (const actor of [...actors].reverse()) {
477
+ try {
478
+ await actor.stop();
479
+ } catch {
480
+ }
481
+ }
482
+ throw error;
202
483
  }
203
- return actors;
204
484
  }
205
485
 
206
486
  // src/locator/createDappLocator.ts
@@ -208,77 +488,318 @@ import { ConsoleLogger } from "@ariestools/sdk";
208
488
 
209
489
  // src/providers/DappBucketStore.ts
210
490
  import {
491
+ DeleteObjectCommand,
211
492
  GetObjectCommand,
493
+ HeadObjectCommand,
212
494
  ListObjectsV2Command,
213
495
  PutObjectCommand,
214
496
  S3Client
215
497
  } from "@aws-sdk/client-s3";
498
+
499
+ // src/providers/errors.ts
500
+ var ConditionalWriteError = class extends Error {
501
+ key;
502
+ name = "ConditionalWriteError";
503
+ constructor(key, message) {
504
+ super(message ?? `Conditional write failed for key "${key}"`);
505
+ this.key = key;
506
+ }
507
+ };
508
+ function isConditionalWriteError(error) {
509
+ return error instanceof ConditionalWriteError || error !== null && typeof error === "object" && error.name === "ConditionalWriteError";
510
+ }
511
+
512
+ // src/providers/joinKey.ts
513
+ function joinKey(prefix, relativeKey) {
514
+ const cleanPrefix = prefix.replaceAll(/^\/+|\/+$/g, "");
515
+ const cleanKey = relativeKey.replaceAll(/^\/+/g, "");
516
+ if (cleanPrefix.length === 0) return cleanKey;
517
+ if (cleanKey.length === 0) return cleanPrefix;
518
+ return `${cleanPrefix}/${cleanKey}`;
519
+ }
520
+ function stripPrefix(prefix, physicalKey) {
521
+ const cleanPrefix = prefix.replaceAll(/^\/+|\/+$/g, "");
522
+ if (cleanPrefix.length === 0) return physicalKey;
523
+ const withSlash = `${cleanPrefix}/`;
524
+ if (physicalKey === cleanPrefix) return "";
525
+ if (physicalKey.startsWith(withSlash)) return physicalKey.slice(withSlash.length);
526
+ return physicalKey;
527
+ }
528
+ function normalizePublicBaseUrl(url) {
529
+ if (url === void 0 || url.length === 0) return void 0;
530
+ return url.replace(/\/+$/, "");
531
+ }
532
+
533
+ // src/providers/DappBucketStore.ts
216
534
  var DEFAULT_CREDENTIAL = "S3RVER";
535
+ var DEFAULT_REGION = "us-east-1";
217
536
  function isNotFound(error) {
218
537
  const name = error.name;
219
- return name === "NoSuchKey" || name === "NotFound";
538
+ const status = error.$metadata?.httpStatusCode;
539
+ return name === "NoSuchKey" || name === "NotFound" || name === "404" || status === 404;
220
540
  }
221
- function createS3BucketStore(options) {
222
- const { bucket } = options;
541
+ function isConditionalFailure(error) {
542
+ const name = error.name;
543
+ const status = error.$metadata?.httpStatusCode;
544
+ return name === "PreconditionFailed" || name === "ConditionalRequestConflict" || name === "412" || status === 412;
545
+ }
546
+ function toBodyBytes(body) {
547
+ return typeof body === "string" ? new TextEncoder().encode(body) : body;
548
+ }
549
+ function metaFromResponse(response) {
550
+ return {
551
+ ...response.CacheControl !== void 0 && { cacheControl: response.CacheControl },
552
+ ...response.ContentEncoding !== void 0 && { contentEncoding: response.ContentEncoding },
553
+ ...response.ContentLength !== void 0 && { contentLength: response.ContentLength },
554
+ ...response.ContentType !== void 0 && { contentType: response.ContentType },
555
+ ...response.ETag !== void 0 && { etag: response.ETag },
556
+ ...response.LastModified !== void 0 && { lastModified: response.LastModified },
557
+ ...response.Metadata !== void 0 && Object.keys(response.Metadata).length > 0 && { metadata: response.Metadata }
558
+ };
559
+ }
560
+ function resolveClientCredentials(options) {
561
+ if (options.credentials !== void 0) return options.credentials;
562
+ if (options.endpoint === void 0) return void 0;
563
+ return {
564
+ accessKeyId: DEFAULT_CREDENTIAL,
565
+ secretAccessKey: DEFAULT_CREDENTIAL
566
+ };
567
+ }
568
+ function resolveS3Client(options) {
569
+ if (options.client !== void 0) {
570
+ return {
571
+ client: options.client,
572
+ ownClient: options.ownClient ?? false
573
+ };
574
+ }
575
+ const credentials = resolveClientCredentials(options);
223
576
  const client = new S3Client({
224
- endpoint: options.endpoint,
225
- region: "us-east-1",
226
- forcePathStyle: true,
227
- credentials: {
228
- accessKeyId: options.accessKeyId ?? DEFAULT_CREDENTIAL,
229
- secretAccessKey: options.secretAccessKey ?? DEFAULT_CREDENTIAL
230
- }
577
+ ...options.endpoint !== void 0 && { endpoint: options.endpoint },
578
+ region: options.region ?? DEFAULT_REGION,
579
+ forcePathStyle: options.forcePathStyle ?? options.endpoint !== void 0,
580
+ ...credentials !== void 0 && { credentials }
231
581
  });
232
582
  return {
583
+ client,
584
+ ownClient: options.ownClient ?? true
585
+ };
586
+ }
587
+ function createS3BucketStore(options) {
588
+ const { role } = options;
589
+ const bindingPrefix = (options.binding.prefix ?? "").replaceAll(/^\/+|\/+$/g, "");
590
+ const publicBaseUrl = normalizePublicBaseUrl(options.binding.publicBaseUrl);
591
+ const { client, ownClient } = resolveS3Client(options);
592
+ const bucket = options.binding.bucket;
593
+ let destroyed = false;
594
+ function resolve(relativeKey) {
595
+ return joinKey(bindingPrefix, relativeKey);
596
+ }
597
+ return {
598
+ role,
233
599
  bucket,
600
+ prefix: bindingPrefix,
601
+ ...publicBaseUrl !== void 0 && { publicBaseUrl },
602
+ resolveKey(key) {
603
+ return resolve(key);
604
+ },
605
+ publicUrl(key) {
606
+ if (publicBaseUrl === void 0) return void 0;
607
+ const relative = key.replaceAll(/^\/+/g, "");
608
+ return relative.length === 0 ? publicBaseUrl : `${publicBaseUrl}/${relative}`;
609
+ },
610
+ destroy() {
611
+ if (destroyed) return;
612
+ destroyed = true;
613
+ if (ownClient) client.destroy();
614
+ },
234
615
  async get(key) {
616
+ const result = await this.getWithMeta(key);
617
+ return result?.body;
618
+ },
619
+ async getWithMeta(key) {
235
620
  try {
236
- const response = await client.send(new GetObjectCommand({ Bucket: bucket, Key: key }));
237
- return await response.Body?.transformToByteArray();
621
+ const response = await client.send(new GetObjectCommand({
622
+ Bucket: bucket,
623
+ Key: resolve(key)
624
+ }));
625
+ const body = await response.Body?.transformToByteArray();
626
+ if (body === void 0) return void 0;
627
+ return {
628
+ body,
629
+ meta: metaFromResponse(response)
630
+ };
238
631
  } catch (error) {
239
632
  if (isNotFound(error)) return void 0;
240
633
  throw error;
241
634
  }
242
635
  },
243
- async put(key, body) {
244
- await client.send(new PutObjectCommand({
636
+ async stat(key) {
637
+ try {
638
+ const response = await client.send(new HeadObjectCommand({
639
+ Bucket: bucket,
640
+ Key: resolve(key)
641
+ }));
642
+ return metaFromResponse(response);
643
+ } catch (error) {
644
+ if (isNotFound(error)) return void 0;
645
+ throw error;
646
+ }
647
+ },
648
+ async put(key, body, putOptions) {
649
+ const physicalKey = resolve(key);
650
+ try {
651
+ const response = await client.send(new PutObjectCommand({
652
+ Bucket: bucket,
653
+ Key: physicalKey,
654
+ Body: toBodyBytes(body),
655
+ ...putOptions?.contentType !== void 0 && { ContentType: putOptions.contentType },
656
+ ...putOptions?.contentEncoding !== void 0 && { ContentEncoding: putOptions.contentEncoding },
657
+ ...putOptions?.cacheControl !== void 0 && { CacheControl: putOptions.cacheControl },
658
+ ...putOptions?.metadata !== void 0 && { Metadata: putOptions.metadata },
659
+ ...putOptions?.ifNotExists === true && { IfNoneMatch: "*" },
660
+ ...putOptions?.ifMatch !== void 0 && { IfMatch: putOptions.ifMatch }
661
+ }));
662
+ return { ...response.ETag !== void 0 && { etag: response.ETag } };
663
+ } catch (error) {
664
+ if (isConditionalFailure(error)) {
665
+ throw new ConditionalWriteError(key);
666
+ }
667
+ throw error;
668
+ }
669
+ },
670
+ async delete(key) {
671
+ await client.send(new DeleteObjectCommand({
245
672
  Bucket: bucket,
246
- Key: key,
247
- Body: body
673
+ Key: resolve(key)
248
674
  }));
249
675
  },
250
676
  async list(prefix) {
251
677
  const keys = [];
678
+ for await (const key of this.listAsync(prefix)) {
679
+ keys.push(key);
680
+ }
681
+ return keys;
682
+ },
683
+ async listPage(pageOptions = {}) {
684
+ const physicalPrefix = pageOptions.prefix === void 0 ? bindingPrefix.length === 0 ? void 0 : `${bindingPrefix}/` : resolve(pageOptions.prefix);
685
+ const response = await client.send(new ListObjectsV2Command({
686
+ Bucket: bucket,
687
+ ...physicalPrefix !== void 0 && { Prefix: physicalPrefix },
688
+ ...pageOptions.continuationToken !== void 0 && { ContinuationToken: pageOptions.continuationToken },
689
+ ...pageOptions.maxKeys !== void 0 && { MaxKeys: pageOptions.maxKeys }
690
+ }));
691
+ const keys = [];
692
+ for (const item of response.Contents ?? []) {
693
+ if (item.Key === void 0) continue;
694
+ keys.push(stripPrefix(bindingPrefix, item.Key));
695
+ }
696
+ const isTruncated = response.IsTruncated === true;
697
+ return {
698
+ keys,
699
+ isTruncated,
700
+ ...isTruncated && response.NextContinuationToken !== void 0 && { continuationToken: response.NextContinuationToken }
701
+ };
702
+ },
703
+ async *listAsync(prefix) {
252
704
  let token;
253
705
  do {
254
- const response = await client.send(new ListObjectsV2Command({
255
- Bucket: bucket,
256
- Prefix: prefix,
257
- ContinuationToken: token
258
- }));
259
- for (const item of response.Contents ?? []) {
260
- if (item.Key !== void 0) keys.push(item.Key);
261
- }
262
- token = response.IsTruncated === true ? response.NextContinuationToken : void 0;
706
+ const page = await this.listPage({
707
+ ...prefix !== void 0 && { prefix },
708
+ ...token !== void 0 && { continuationToken: token }
709
+ });
710
+ for (const key of page.keys) yield key;
711
+ token = page.continuationToken;
263
712
  } while (token !== void 0);
264
- return keys;
265
713
  }
266
714
  };
267
715
  }
268
716
 
269
717
  // src/locator/createDappLocator.ts
718
+ var DEFAULT_BINDINGS = {
719
+ data: { bucket: "data" },
720
+ state: { bucket: "state" },
721
+ index: { bucket: "index" }
722
+ };
723
+ var ROLE_MONIKERS = {
724
+ data: DappDataStoreMoniker,
725
+ state: DappStateStoreMoniker,
726
+ index: DappIndexStoreMoniker
727
+ };
728
+ function resolveCredentials(options) {
729
+ if (options.credentials !== void 0) return options.credentials;
730
+ if (options.accessKeyId !== void 0 || options.secretAccessKey !== void 0) {
731
+ return {
732
+ accessKeyId: options.accessKeyId ?? "S3RVER",
733
+ secretAccessKey: options.secretAccessKey ?? "S3RVER"
734
+ };
735
+ }
736
+ return void 0;
737
+ }
738
+ function resolveSharedClient(options) {
739
+ const credentials = resolveCredentials(options);
740
+ if (options.client !== void 0) {
741
+ return {
742
+ client: options.client,
743
+ ownClient: options.ownClient ?? false
744
+ };
745
+ }
746
+ return resolveS3Client({
747
+ ...options.endpoint !== void 0 && { endpoint: options.endpoint },
748
+ ...options.region !== void 0 && { region: options.region },
749
+ ...options.forcePathStyle !== void 0 && { forcePathStyle: options.forcePathStyle },
750
+ ...credentials !== void 0 && { credentials },
751
+ ownClient: options.ownClient
752
+ });
753
+ }
754
+ function resolveRoleClient(role, options, shared) {
755
+ const override = options.clients?.[role];
756
+ if (override === void 0) return shared;
757
+ if (typeof override.send === "function") {
758
+ return {
759
+ client: override,
760
+ ownClient: false
761
+ };
762
+ }
763
+ return resolveS3Client({
764
+ ...override,
765
+ // Per-role constructed clients are owned by the locator.
766
+ ownClient: override.ownClient ?? true
767
+ });
768
+ }
270
769
  function createDappLocator(options) {
271
770
  const logger = options.logger ?? new ConsoleLogger();
272
- const bucketOptions = {
273
- endpoint: options.endpoint,
274
- ...options.accessKeyId !== void 0 && { accessKeyId: options.accessKeyId },
275
- ...options.secretAccessKey !== void 0 && { secretAccessKey: options.secretAccessKey }
276
- };
277
- const instances = /* @__PURE__ */ new Map([
278
- [DappDataStoreMoniker, createS3BucketStore({ ...bucketOptions, bucket: "data" })],
279
- [DappStateStoreMoniker, createS3BucketStore({ ...bucketOptions, bucket: "state" })],
280
- [DappIndexStoreMoniker, createS3BucketStore({ ...bucketOptions, bucket: "index" })]
281
- ]);
771
+ const bindings = options.bindings ?? DEFAULT_BINDINGS;
772
+ const shared = resolveSharedClient(options);
773
+ const readOnlyData = options.readOnlyData !== false;
774
+ const roles = ["data", "state", "index"];
775
+ const instances = /* @__PURE__ */ new Map();
776
+ const stores = [];
777
+ const ownedClients = [];
778
+ const seenClients = /* @__PURE__ */ new Set();
779
+ const trackClient = (owned) => {
780
+ if (!owned.ownClient || seenClients.has(owned.client)) return;
781
+ seenClients.add(owned.client);
782
+ ownedClients.push(owned);
783
+ };
784
+ trackClient(shared);
785
+ for (const role of roles) {
786
+ const binding = bindings[role];
787
+ const roleClient = resolveRoleClient(role, options, shared);
788
+ trackClient(roleClient);
789
+ let store = createS3BucketStore({
790
+ role,
791
+ binding,
792
+ client: roleClient.client,
793
+ // Locator owns clients; individual stores must not destroy them.
794
+ ownClient: false
795
+ });
796
+ if (role === "data" && readOnlyData) {
797
+ store = asReadOnlyWriter(store);
798
+ }
799
+ stores.push(store);
800
+ instances.set(ROLE_MONIKERS[role], store);
801
+ }
802
+ let destroyed = false;
282
803
  return {
283
804
  context: { logger },
284
805
  has(moniker) {
@@ -294,7 +815,1101 @@ function createDappLocator(options) {
294
815
  },
295
816
  async tryGetInstance(moniker) {
296
817
  return instances.get(moniker);
818
+ },
819
+ destroy() {
820
+ if (destroyed) return;
821
+ destroyed = true;
822
+ for (const store of stores) store.destroy();
823
+ for (const owned of ownedClients) {
824
+ if (owned.ownClient) owned.client.destroy();
825
+ }
826
+ }
827
+ };
828
+ }
829
+
830
+ // src/providers/hashBytes.ts
831
+ import { createHash } from "node:crypto";
832
+ function hashBytes(body) {
833
+ const hash = createHash("sha256");
834
+ hash.update(typeof body === "string" ? body : Buffer.from(body));
835
+ return hash.digest("hex");
836
+ }
837
+
838
+ // src/providers/memoryBucketStore.ts
839
+ function createMemoryBucketStore(options) {
840
+ const objects = /* @__PURE__ */ new Map();
841
+ let etagSeq = 0;
842
+ const role = options.role;
843
+ const bucket = options.bucket ?? role;
844
+ const prefix = (options.prefix ?? "").replaceAll(/^\/+|\/+$/g, "");
845
+ const publicBaseUrl = options.publicBaseUrl;
846
+ function nextEtag() {
847
+ etagSeq += 1;
848
+ return `"mem-${etagSeq}"`;
849
+ }
850
+ function toBytes4(body) {
851
+ return typeof body === "string" ? new TextEncoder().encode(body) : body;
852
+ }
853
+ return {
854
+ role,
855
+ bucket,
856
+ prefix,
857
+ ...publicBaseUrl !== void 0 && { publicBaseUrl },
858
+ destroy() {
859
+ objects.clear();
860
+ },
861
+ resolveKey(key) {
862
+ const clean = key.replaceAll(/^\/+/g, "");
863
+ if (prefix.length === 0) return clean;
864
+ return clean.length === 0 ? prefix : `${prefix}/${clean}`;
865
+ },
866
+ publicUrl(key) {
867
+ if (publicBaseUrl === void 0) return void 0;
868
+ const relative = key.replaceAll(/^\/+/g, "");
869
+ return relative.length === 0 ? publicBaseUrl : `${publicBaseUrl}/${relative}`;
870
+ },
871
+ async get(key) {
872
+ return objects.get(key)?.body;
873
+ },
874
+ async getWithMeta(key) {
875
+ const found = objects.get(key);
876
+ if (found === void 0) return void 0;
877
+ return {
878
+ body: found.body,
879
+ meta: {
880
+ etag: found.etag,
881
+ ...found.contentType !== void 0 && { contentType: found.contentType },
882
+ ...found.contentEncoding !== void 0 && { contentEncoding: found.contentEncoding },
883
+ ...found.cacheControl !== void 0 && { cacheControl: found.cacheControl },
884
+ contentLength: found.body.byteLength,
885
+ ...found.metadata !== void 0 && { metadata: found.metadata }
886
+ }
887
+ };
888
+ },
889
+ async stat(key) {
890
+ const found = objects.get(key);
891
+ if (found === void 0) return void 0;
892
+ return {
893
+ etag: found.etag,
894
+ contentLength: found.body.byteLength,
895
+ ...found.contentType !== void 0 && { contentType: found.contentType },
896
+ ...found.cacheControl !== void 0 && { cacheControl: found.cacheControl }
897
+ };
898
+ },
899
+ async put(key, body, putOptions) {
900
+ const existing = objects.get(key);
901
+ if (putOptions?.ifNotExists === true && existing !== void 0) {
902
+ throw new ConditionalWriteError(key);
903
+ }
904
+ if (putOptions?.ifMatch !== void 0 && (existing === void 0 || existing.etag !== putOptions.ifMatch)) {
905
+ throw new ConditionalWriteError(key);
906
+ }
907
+ const etag = nextEtag();
908
+ objects.set(key, {
909
+ body: toBytes4(body),
910
+ etag,
911
+ ...putOptions?.contentType !== void 0 && { contentType: putOptions.contentType },
912
+ ...putOptions?.contentEncoding !== void 0 && { contentEncoding: putOptions.contentEncoding },
913
+ ...putOptions?.cacheControl !== void 0 && { cacheControl: putOptions.cacheControl },
914
+ ...putOptions?.metadata !== void 0 && { metadata: putOptions.metadata }
915
+ });
916
+ return { etag };
917
+ },
918
+ async delete(key) {
919
+ objects.delete(key);
920
+ },
921
+ async list(listPrefix) {
922
+ const keys = [];
923
+ for await (const key of this.listAsync(listPrefix)) keys.push(key);
924
+ return keys;
925
+ },
926
+ async listPage(pageOptions = {}) {
927
+ const p = pageOptions.prefix ?? "";
928
+ let keys = [...objects.keys()].filter((k) => k.startsWith(p)).sort();
929
+ if (pageOptions.continuationToken !== void 0) {
930
+ const idx = keys.indexOf(pageOptions.continuationToken);
931
+ keys = idx === -1 ? keys : keys.slice(idx + 1);
932
+ }
933
+ const max = pageOptions.maxKeys ?? keys.length;
934
+ const page = keys.slice(0, max);
935
+ const isTruncated = keys.length > max;
936
+ return {
937
+ keys: page,
938
+ isTruncated,
939
+ ...isTruncated && page.length > 0 && { continuationToken: page.at(-1) }
940
+ };
941
+ },
942
+ async *listAsync(listPrefix) {
943
+ let token;
944
+ do {
945
+ const page = await this.listPage({
946
+ ...listPrefix !== void 0 && { prefix: listPrefix },
947
+ ...token !== void 0 && { continuationToken: token },
948
+ maxKeys: 100
949
+ });
950
+ for (const key of page.keys) yield key;
951
+ token = page.continuationToken;
952
+ } while (token !== void 0);
953
+ }
954
+ };
955
+ }
956
+
957
+ // src/providers/types.ts
958
+ var DAPP_STORE_ROLES = ["data", "state", "index"];
959
+
960
+ // src/publication/canonicalHead.ts
961
+ function canonicalHeadBytes(head) {
962
+ const payload = {
963
+ schema: head.schema,
964
+ generation: head.generation,
965
+ manifestKey: head.manifestKey,
966
+ manifestHash: head.manifestHash,
967
+ publishedAt: head.publishedAt
968
+ };
969
+ if (head.previousGeneration !== void 0) {
970
+ payload.previousGeneration = head.previousGeneration;
971
+ }
972
+ if (head.revision !== void 0) payload.revision = head.revision;
973
+ if (head.trust !== void 0) payload.trust = head.trust;
974
+ if (head.layout !== void 0) payload.layout = head.layout;
975
+ const ordered = {};
976
+ for (const key of [
977
+ "schema",
978
+ "generation",
979
+ "manifestKey",
980
+ "manifestHash",
981
+ "publishedAt",
982
+ "previousGeneration",
983
+ "revision",
984
+ "trust",
985
+ "layout"
986
+ ]) {
987
+ if (key in payload) ordered[key] = payload[key];
988
+ }
989
+ return new TextEncoder().encode(`${JSON.stringify(ordered)}
990
+ `);
991
+ }
992
+
993
+ // src/publication/publishGeneration.ts
994
+ import { randomUUID } from "node:crypto";
995
+
996
+ // src/publication/commitHead.ts
997
+ async function commitHead(state, head, safety, fenced, expectedHeadEtag) {
998
+ const headBody = `${JSON.stringify(head, null, 2)}
999
+ `;
1000
+ const base = {
1001
+ contentType: "application/json",
1002
+ cacheControl: CACHE_CONTROL_REVALIDATE
1003
+ };
1004
+ try {
1005
+ if (!fenced) {
1006
+ return await state.put(HEAD_KEY, headBody, base);
1007
+ }
1008
+ if (expectedHeadEtag === void 0) {
1009
+ return await state.put(HEAD_KEY, headBody, { ...base, ifNotExists: true });
1010
+ }
1011
+ return await state.put(HEAD_KEY, headBody, { ...base, ifMatch: expectedHeadEtag });
1012
+ } catch (error) {
1013
+ if (error instanceof ConditionalWriteError) {
1014
+ return {
1015
+ conflict: {
1016
+ kind: safety.mode === "lease" ? "lease" : "head-cas",
1017
+ message: `Head publish conflict on "${HEAD_KEY}" (stale or concurrent writer)`,
1018
+ key: HEAD_KEY
1019
+ }
1020
+ };
1021
+ }
1022
+ throw error;
1023
+ }
1024
+ }
1025
+
1026
+ // src/publication/safety.ts
1027
+ var PROBE_PREFIX = ".aries-dapp-probe/";
1028
+ async function probePublicationCapabilities(store) {
1029
+ const key = `${PROBE_PREFIX}${Date.now()}-${Math.random().toString(36).slice(2)}`;
1030
+ let conditionalCreate = false;
1031
+ let conditionalUpdate = false;
1032
+ try {
1033
+ const created = await store.put(key, "probe-v1", {
1034
+ ifNotExists: true,
1035
+ contentType: "text/plain"
1036
+ });
1037
+ try {
1038
+ await store.put(key, "probe-v1-again", { ifNotExists: true });
1039
+ } catch (error) {
1040
+ conditionalCreate = error instanceof ConditionalWriteError;
1041
+ }
1042
+ const etag = created.etag;
1043
+ if (etag !== void 0) {
1044
+ try {
1045
+ await store.put(key, "probe-v2", {
1046
+ ifMatch: etag,
1047
+ contentType: "text/plain"
1048
+ });
1049
+ try {
1050
+ await store.put(key, "probe-stale", {
1051
+ ifMatch: '"not-the-etag"',
1052
+ contentType: "text/plain"
1053
+ });
1054
+ } catch (error) {
1055
+ conditionalUpdate = error instanceof ConditionalWriteError;
1056
+ }
1057
+ } catch {
1058
+ }
1059
+ }
1060
+ } catch {
1061
+ }
1062
+ try {
1063
+ await store.put(key, "", { contentType: "text/plain" });
1064
+ } catch {
1065
+ }
1066
+ return { conditionalCreate, conditionalUpdate };
1067
+ }
1068
+ async function assertPublicationSafety(store, safety, capabilities) {
1069
+ if (safety.mode === "unfenced") {
1070
+ return { conditionalCreate: false, conditionalUpdate: false };
1071
+ }
1072
+ if (safety.mode === "lease") {
1073
+ return capabilities ?? await probePublicationCapabilities(store);
1074
+ }
1075
+ const caps = capabilities ?? await probePublicationCapabilities(store);
1076
+ if (!caps.conditionalCreate || !caps.conditionalUpdate) {
1077
+ throw new Error(
1078
+ 'Publication safety mode "conditional-head" requires provider support for create-only (If-None-Match: *) and conditional update (If-Match). Probe failed; refuse to publish without fencing. Use an injected lease adapter, or fix provider configuration.'
1079
+ );
1080
+ }
1081
+ return caps;
1082
+ }
1083
+
1084
+ // src/publication/putCreateOnly.ts
1085
+ function toBytes(body) {
1086
+ return typeof body === "string" ? new TextEncoder().encode(body) : body;
1087
+ }
1088
+ function bytesEqual(a, b) {
1089
+ if (a.byteLength !== b.byteLength) return false;
1090
+ for (let i = 0; i < a.byteLength; i++) {
1091
+ if (a[i] !== b[i]) return false;
1092
+ }
1093
+ return true;
1094
+ }
1095
+ async function putCreateOnly(store, key, body, options) {
1096
+ const bytes = toBytes(body);
1097
+ const hash = hashBytes(bytes);
1098
+ try {
1099
+ const result = await store.put(key, bytes, {
1100
+ ...options,
1101
+ ifNotExists: true
1102
+ });
1103
+ return { ...result, hash };
1104
+ } catch (error) {
1105
+ if (!(error instanceof ConditionalWriteError)) throw error;
1106
+ const existing = await store.get(key);
1107
+ if (existing === void 0) {
1108
+ throw new Error(
1109
+ `Immutable create-only collision on "${key}" (object missing after precondition failure)`,
1110
+ { cause: error }
1111
+ );
1112
+ }
1113
+ if (bytesEqual(existing, bytes)) {
1114
+ const meta = await store.stat(key);
1115
+ return { hash, ...meta?.etag !== void 0 && { etag: meta.etag } };
1116
+ }
1117
+ throw new Error(
1118
+ `Immutable create-only collision on "${key}": existing bytes differ (wanted hash ${hash}, existing hash ${hashBytes(existing)})`,
1119
+ { cause: error }
1120
+ );
1121
+ }
1122
+ }
1123
+
1124
+ // src/publication/writeGenerationObjects.ts
1125
+ function assertNotAborted(signal) {
1126
+ if (signal?.aborted === true) {
1127
+ throw new Error("Publication aborted");
1128
+ }
1129
+ }
1130
+ function toBytes2(body) {
1131
+ return typeof body === "string" ? new TextEncoder().encode(body) : body;
1132
+ }
1133
+ async function writeImmutableObjects(store, role, generation, objects, signal, fenced) {
1134
+ const refs = [];
1135
+ for (const object of objects) {
1136
+ assertNotAborted(signal);
1137
+ const relativeKey = `${GENERATIONS_PREFIX}/${generation}/${object.key.replaceAll(/^\/+/g, "")}`;
1138
+ const bytes = toBytes2(object.body);
1139
+ const contentType = object.contentType ?? "application/json";
1140
+ const putOpts = {
1141
+ contentType,
1142
+ cacheControl: CACHE_CONTROL_IMMUTABLE,
1143
+ ...object.metadata !== void 0 && { metadata: object.metadata }
1144
+ };
1145
+ let hash;
1146
+ if (fenced) {
1147
+ hash = (await putCreateOnly(store, relativeKey, bytes, putOpts)).hash;
1148
+ } else {
1149
+ hash = hashBytes(bytes);
1150
+ await store.put(relativeKey, bytes, putOpts);
1151
+ }
1152
+ const publicUrl = store.publicUrl(relativeKey);
1153
+ refs.push({
1154
+ role,
1155
+ key: relativeKey,
1156
+ hash,
1157
+ size: bytes.byteLength,
1158
+ contentType,
1159
+ ...publicUrl !== void 0 && { publicUrl }
1160
+ });
1161
+ }
1162
+ return refs;
1163
+ }
1164
+ async function writeManifest(store, manifestKey, manifestBody, fenced) {
1165
+ const opts = {
1166
+ contentType: "application/json",
1167
+ cacheControl: CACHE_CONTROL_IMMUTABLE
1168
+ };
1169
+ if (fenced) {
1170
+ await putCreateOnly(store, manifestKey, manifestBody, opts);
1171
+ return;
1172
+ }
1173
+ await store.put(manifestKey, manifestBody, opts);
1174
+ }
1175
+
1176
+ // src/publication/publishGeneration.ts
1177
+ function assertNotAborted2(signal) {
1178
+ if (signal?.aborted === true) {
1179
+ throw new Error("Publication aborted");
1180
+ }
1181
+ }
1182
+ async function acquireLeaseIfNeeded(input) {
1183
+ if (input.safety.mode !== "lease") return void 0;
1184
+ return await input.safety.lease.acquire({
1185
+ key: HEAD_KEY,
1186
+ ttlMs: 6e4,
1187
+ ...input.signal !== void 0 && { signal: input.signal }
1188
+ });
1189
+ }
1190
+ async function buildAndSignHead(input, generation, manifestKey, manifestHash, publishedAt) {
1191
+ const trustMode = input.trust?.mode ?? "trusted-origin";
1192
+ const head = {
1193
+ schema: HEAD_MANIFEST_SCHEMA,
1194
+ generation,
1195
+ layout: "full",
1196
+ manifestKey,
1197
+ manifestHash,
1198
+ publishedAt,
1199
+ trust: trustMode,
1200
+ ...input.previousGeneration !== void 0 && { previousGeneration: input.previousGeneration },
1201
+ ...input.revision !== void 0 && { revision: input.revision }
1202
+ };
1203
+ if (input.trust?.mode === "signed") {
1204
+ head.signature = await input.trust.sign(canonicalHeadBytes(head));
1205
+ }
1206
+ return head;
1207
+ }
1208
+ async function writeObjectsAndManifest(input, generation, fenced) {
1209
+ const signal = input.signal;
1210
+ const stateRefs = await writeImmutableObjects(input.state, "state", generation, input.stateObjects ?? [], signal, fenced);
1211
+ const indexRefs = await writeImmutableObjects(input.index, "index", generation, input.indexObjects ?? [], signal, fenced);
1212
+ const publishedAt = (/* @__PURE__ */ new Date()).toISOString();
1213
+ const manifest = {
1214
+ schema: GENERATION_MANIFEST_SCHEMA,
1215
+ generation,
1216
+ publishedAt,
1217
+ reducer: {
1218
+ name: input.reducer.name,
1219
+ ...input.reducer.version !== void 0 && { version: input.reducer.version }
1220
+ },
1221
+ ...input.schemaVersion !== void 0 && { schemaVersion: input.schemaVersion },
1222
+ source: input.source ?? {},
1223
+ objects: [...stateRefs, ...indexRefs]
1224
+ };
1225
+ const manifestKey = `${GENERATIONS_PREFIX}/${generation}/manifest.json`;
1226
+ const manifestBody = `${JSON.stringify(manifest, null, 2)}
1227
+ `;
1228
+ const manifestHash = hashBytes(manifestBody);
1229
+ assertNotAborted2(signal);
1230
+ try {
1231
+ await writeManifest(input.state, manifestKey, manifestBody, fenced);
1232
+ } catch (error) {
1233
+ if (error instanceof Error && error.message.includes("Immutable create-only collision")) {
1234
+ return {
1235
+ ok: false,
1236
+ conflict: {
1237
+ kind: "immutable-collision",
1238
+ message: error.message,
1239
+ key: manifestKey
1240
+ }
1241
+ };
1242
+ }
1243
+ throw error;
1244
+ }
1245
+ return {
1246
+ ok: true,
1247
+ manifest,
1248
+ manifestKey,
1249
+ manifestHash
1250
+ };
1251
+ }
1252
+ async function releaseLease(handle) {
1253
+ if (handle === void 0) return;
1254
+ try {
1255
+ await handle.release();
1256
+ } catch {
1257
+ }
1258
+ }
1259
+ async function publishGeneration(input) {
1260
+ const generation = input.generation ?? randomUUID();
1261
+ const signal = input.signal;
1262
+ assertNotAborted2(signal);
1263
+ if (input.assertSafety !== false && input.safety.mode === "conditional-head") {
1264
+ await assertPublicationSafety(input.state, input.safety);
1265
+ }
1266
+ const fenced = input.safety.mode !== "unfenced";
1267
+ const leaseHandle = await acquireLeaseIfNeeded(input);
1268
+ try {
1269
+ const written = await writeObjectsAndManifest(input, generation, fenced);
1270
+ if (!written.ok) return written;
1271
+ const head = await buildAndSignHead(input, generation, written.manifestKey, written.manifestHash, written.manifest.publishedAt);
1272
+ assertNotAborted2(signal);
1273
+ const committed = await commitHead(input.state, head, input.safety, fenced, input.expectedHeadEtag);
1274
+ if ("conflict" in committed) {
1275
+ return { ok: false, conflict: committed.conflict };
1276
+ }
1277
+ return {
1278
+ ok: true,
1279
+ generation,
1280
+ manifest: written.manifest,
1281
+ manifestKey: written.manifestKey,
1282
+ manifestHash: written.manifestHash,
1283
+ head,
1284
+ headKey: HEAD_KEY,
1285
+ ...committed.etag !== void 0 && { headEtag: committed.etag }
1286
+ };
1287
+ } catch (error) {
1288
+ if (error instanceof Error && error.message.includes("Immutable create-only collision")) {
1289
+ return {
1290
+ ok: false,
1291
+ conflict: {
1292
+ kind: "immutable-collision",
1293
+ message: error.message
1294
+ }
1295
+ };
1296
+ }
1297
+ if (signal?.aborted === true) {
1298
+ return {
1299
+ ok: false,
1300
+ conflict: {
1301
+ kind: "aborted",
1302
+ message: error instanceof Error ? error.message : "Publication aborted"
1303
+ }
1304
+ };
1305
+ }
1306
+ throw error;
1307
+ } finally {
1308
+ await releaseLease(leaseHandle);
1309
+ }
1310
+ }
1311
+ async function readPublishedHead(state) {
1312
+ const withMeta = await state.getWithMeta(HEAD_KEY);
1313
+ if (withMeta === void 0) return void 0;
1314
+ const head = JSON.parse(new TextDecoder().decode(withMeta.body));
1315
+ if (head.schema !== HEAD_MANIFEST_SCHEMA) {
1316
+ throw new Error(`Invalid head schema: ${String(head.schema)}`);
1317
+ }
1318
+ const targetBody = await state.get(head.manifestKey);
1319
+ if (targetBody === void 0) {
1320
+ throw new Error(`Head points at missing object "${head.manifestKey}"`);
1321
+ }
1322
+ const targetBytes = typeof targetBody === "string" ? new TextEncoder().encode(targetBody) : targetBody;
1323
+ const actualHash = hashBytes(targetBytes);
1324
+ if (actualHash !== head.manifestHash) {
1325
+ throw new Error(
1326
+ `Head target hash mismatch for generation "${head.generation}": head=${head.manifestHash} actual=${actualHash}`
1327
+ );
1328
+ }
1329
+ if (head.layout === "incremental-cas") {
1330
+ return {
1331
+ head,
1332
+ ...withMeta.meta.etag !== void 0 && { headEtag: withMeta.meta.etag }
1333
+ };
1334
+ }
1335
+ const manifest = JSON.parse(new TextDecoder().decode(targetBytes));
1336
+ if (manifest.schema !== GENERATION_MANIFEST_SCHEMA) {
1337
+ throw new Error(`Invalid generation manifest schema: ${String(manifest.schema)}`);
1338
+ }
1339
+ if (manifest.generation !== head.generation) {
1340
+ throw new Error(
1341
+ `Generation id mismatch: head=${head.generation} manifest=${manifest.generation}`
1342
+ );
1343
+ }
1344
+ return {
1345
+ head,
1346
+ manifest,
1347
+ ...withMeta.meta.etag !== void 0 && { headEtag: withMeta.meta.etag }
1348
+ };
1349
+ }
1350
+
1351
+ // src/publication/releases.ts
1352
+ async function readPins(state) {
1353
+ const found = await state.getWithMeta(PINS_KEY);
1354
+ if (found === void 0) {
1355
+ return {
1356
+ doc: {
1357
+ schema: PINS_SCHEMA,
1358
+ pins: [],
1359
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString()
1360
+ }
1361
+ };
1362
+ }
1363
+ const doc = JSON.parse(new TextDecoder().decode(found.body));
1364
+ return {
1365
+ doc,
1366
+ ...found.meta.etag !== void 0 && { etag: found.meta.etag }
1367
+ };
1368
+ }
1369
+ async function writePins(state, doc, expectedEtag) {
1370
+ const body = `${JSON.stringify(doc, null, 2)}
1371
+ `;
1372
+ const opts = {
1373
+ contentType: "application/json",
1374
+ cacheControl: CACHE_CONTROL_REVALIDATE,
1375
+ ...expectedEtag === void 0 ? { ifNotExists: true } : { ifMatch: expectedEtag }
1376
+ };
1377
+ try {
1378
+ await state.put(PINS_KEY, body, opts);
1379
+ } catch (error) {
1380
+ if (error instanceof ConditionalWriteError && expectedEtag === void 0) {
1381
+ }
1382
+ throw error;
1383
+ }
1384
+ }
1385
+ async function listReleases(state) {
1386
+ const byGen = /* @__PURE__ */ new Map();
1387
+ for await (const key of state.listAsync(`${GENERATIONS_PREFIX}/`)) {
1388
+ const parts = key.split("/");
1389
+ if (parts.length < 3 || parts[0] !== GENERATIONS_PREFIX) continue;
1390
+ const generation = parts[1];
1391
+ const existing = byGen.get(generation) ?? { generation, kind: "full" };
1392
+ if (parts[2] === "manifest.json") {
1393
+ existing.manifestKey = key;
1394
+ existing.kind = "full";
1395
+ }
1396
+ byGen.set(generation, existing);
1397
+ }
1398
+ for await (const key of state.listAsync(`${RECEIPTS_PREFIX}/`)) {
1399
+ const base = key.slice(RECEIPTS_PREFIX.length + 1).replace(/\.json$/, "");
1400
+ if (base.length === 0) continue;
1401
+ const existing = byGen.get(base) ?? { generation: base, kind: "incremental" };
1402
+ existing.receiptKey = key;
1403
+ if (existing.manifestKey === void 0) existing.kind = "incremental";
1404
+ byGen.set(base, existing);
1405
+ }
1406
+ return [...byGen.values()].sort((a, b) => a.generation.localeCompare(b.generation));
1407
+ }
1408
+ async function pinRelease(state, generation, opts) {
1409
+ const { doc, etag } = await readPins(state);
1410
+ if (doc.pins.some((p) => p.generation === generation)) return doc;
1411
+ const next = {
1412
+ schema: PINS_SCHEMA,
1413
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1414
+ pins: [
1415
+ ...doc.pins,
1416
+ {
1417
+ generation,
1418
+ pinnedAt: (/* @__PURE__ */ new Date()).toISOString(),
1419
+ ...opts?.reason !== void 0 && { reason: opts.reason }
1420
+ }
1421
+ ]
1422
+ };
1423
+ await writePins(state, next, etag);
1424
+ return next;
1425
+ }
1426
+ async function unpinRelease(state, generation) {
1427
+ const { doc, etag } = await readPins(state);
1428
+ const next = {
1429
+ schema: PINS_SCHEMA,
1430
+ updatedAt: (/* @__PURE__ */ new Date()).toISOString(),
1431
+ pins: doc.pins.filter((p) => p.generation !== generation)
1432
+ };
1433
+ await writePins(state, next, etag);
1434
+ return next;
1435
+ }
1436
+ async function listPins(state) {
1437
+ return (await readPins(state)).doc.pins;
1438
+ }
1439
+ async function rollbackHead(state, input) {
1440
+ const current = await readPublishedHead(state);
1441
+ if (current === void 0) {
1442
+ return { ok: false, message: "No current head to roll back" };
1443
+ }
1444
+ if (current.head.generation === input.targetGeneration) {
1445
+ return {
1446
+ ok: true,
1447
+ head: current.head,
1448
+ ...current.headEtag !== void 0 && { headEtag: current.headEtag }
1449
+ };
1450
+ }
1451
+ const manifestKey = `${GENERATIONS_PREFIX}/${input.targetGeneration}/manifest.json`;
1452
+ const receiptKey = `${RECEIPTS_PREFIX}/${input.targetGeneration}.json`;
1453
+ const manifestBody = await state.get(manifestKey);
1454
+ const receiptBody = manifestBody === void 0 ? await state.get(receiptKey) : void 0;
1455
+ let head;
1456
+ if (manifestBody !== void 0) {
1457
+ const bytes = typeof manifestBody === "string" ? new TextEncoder().encode(manifestBody) : manifestBody;
1458
+ const parsed = JSON.parse(new TextDecoder().decode(bytes));
1459
+ if (parsed.generation !== input.targetGeneration) {
1460
+ return { ok: false, message: "Target manifest generation mismatch" };
1461
+ }
1462
+ head = {
1463
+ schema: HEAD_MANIFEST_SCHEMA,
1464
+ generation: input.targetGeneration,
1465
+ manifestKey,
1466
+ manifestHash: hashBytes(bytes),
1467
+ publishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1468
+ previousGeneration: current.head.generation,
1469
+ trust: current.head.trust ?? "trusted-origin",
1470
+ revision: (current.head.revision ?? 1) + 1,
1471
+ layout: "full"
1472
+ };
1473
+ } else if (receiptBody === void 0) {
1474
+ return { ok: false, message: `Target generation "${input.targetGeneration}" not found` };
1475
+ } else {
1476
+ const receipt = JSON.parse(new TextDecoder().decode(receiptBody));
1477
+ head = {
1478
+ schema: HEAD_MANIFEST_SCHEMA,
1479
+ generation: receipt.generation,
1480
+ manifestKey: receipt.rootKey,
1481
+ manifestHash: receipt.rootHash,
1482
+ publishedAt: (/* @__PURE__ */ new Date()).toISOString(),
1483
+ previousGeneration: current.head.generation,
1484
+ trust: current.head.trust ?? "trusted-origin",
1485
+ revision: (current.head.revision ?? 1) + 1,
1486
+ layout: "incremental-cas"
1487
+ };
1488
+ }
1489
+ const fenced = input.safety.mode !== "unfenced";
1490
+ const etag = input.expectedHeadEtag ?? current.headEtag;
1491
+ const committed = await commitHead(state, head, input.safety, fenced, etag);
1492
+ if ("conflict" in committed) {
1493
+ return { ok: false, message: committed.conflict.message };
1494
+ }
1495
+ return {
1496
+ ok: true,
1497
+ head,
1498
+ ...committed.etag !== void 0 && { headEtag: committed.etag }
1499
+ };
1500
+ }
1501
+
1502
+ // src/publication/gc.ts
1503
+ var PROTECTED_KEYS = /* @__PURE__ */ new Set([HEAD_KEY, STATUS_KEY, PINS_KEY]);
1504
+ function generationFromKey(key) {
1505
+ if (key.startsWith(`${GENERATIONS_PREFIX}/`)) {
1506
+ return key.split("/", 2)[1];
1507
+ }
1508
+ if (key.startsWith(`${RECEIPTS_PREFIX}/`) && key.endsWith(".json")) {
1509
+ return key.slice(RECEIPTS_PREFIX.length + 1, -".json".length);
1510
+ }
1511
+ return void 0;
1512
+ }
1513
+ async function collectGenerationKeys(store, generation) {
1514
+ const keys = [];
1515
+ for await (const key of store.listAsync(`${GENERATIONS_PREFIX}/${generation}/`)) {
1516
+ keys.push(key);
1517
+ }
1518
+ const receipt = `${RECEIPTS_PREFIX}/${generation}.json`;
1519
+ if (await store.stat(receipt) !== void 0) keys.push(receipt);
1520
+ return keys;
1521
+ }
1522
+ async function collectCasAndMapFromHead(state, index, generation) {
1523
+ const stateKeys = [];
1524
+ const indexKeys = [];
1525
+ stateKeys.push(...await collectGenerationKeys(state, generation));
1526
+ if (index !== void 0) {
1527
+ for await (const key of index.listAsync(`${GENERATIONS_PREFIX}/${generation}/`)) {
1528
+ indexKeys.push(key);
1529
+ }
1530
+ }
1531
+ const receiptKey = `${RECEIPTS_PREFIX}/${generation}.json`;
1532
+ const receiptBody = await state.get(receiptKey);
1533
+ if (receiptBody === void 0) {
1534
+ return { stateKeys, indexKeys };
1535
+ }
1536
+ stateKeys.push(receiptKey);
1537
+ const receipt = JSON.parse(new TextDecoder().decode(receiptBody));
1538
+ if (receipt.rootKey !== void 0) stateKeys.push(receipt.rootKey);
1539
+ if (receipt.rootHash !== void 0) {
1540
+ const rootKey = receipt.rootKey ?? `${MAP_ROOTS_PREFIX}/${receipt.rootHash}.json`;
1541
+ const rootBody = await state.get(rootKey);
1542
+ if (rootBody !== void 0) {
1543
+ const root = JSON.parse(new TextDecoder().decode(rootBody));
1544
+ for (const shardHash of Object.values(root.shards ?? {})) {
1545
+ const shardKey = `${MAP_SHARDS_PREFIX}/${shardHash}.json`;
1546
+ stateKeys.push(shardKey);
1547
+ const shardBody = await state.get(shardKey) ?? (index === void 0 ? void 0 : await index.get(shardKey));
1548
+ if (shardBody === void 0) continue;
1549
+ const shard = JSON.parse(new TextDecoder().decode(shardBody));
1550
+ for (const entry of Object.values(shard.entries ?? {})) {
1551
+ const casKey = `${CAS_PREFIX}/${entry.hash}`;
1552
+ indexKeys.push(casKey);
1553
+ stateKeys.push(casKey);
1554
+ }
1555
+ }
1556
+ }
1557
+ }
1558
+ for (const obj of receipt.objects ?? []) {
1559
+ if (obj.role === "index") indexKeys.push(obj.key);
1560
+ else stateKeys.push(obj.key);
1561
+ }
1562
+ return { stateKeys, indexKeys };
1563
+ }
1564
+ async function planGarbageCollection(options) {
1565
+ const {
1566
+ state,
1567
+ index,
1568
+ policy,
1569
+ inFlightGeneration
1570
+ } = options;
1571
+ if (policy.mode === "forever") {
1572
+ return {
1573
+ policy,
1574
+ deleteKeys: [],
1575
+ retainKeys: []
1576
+ };
1577
+ }
1578
+ const head = await readPublishedHead(state);
1579
+ const pins = await listPins(state);
1580
+ const protectedGens = /* @__PURE__ */ new Set();
1581
+ if (head !== void 0) protectedGens.add(head.head.generation);
1582
+ if (head?.head.previousGeneration !== void 0) {
1583
+ protectedGens.add(head.head.previousGeneration);
1584
+ }
1585
+ for (const pin of pins) protectedGens.add(pin.generation);
1586
+ if (inFlightGeneration !== void 0) protectedGens.add(inFlightGeneration);
1587
+ const allGenKeys = await state.list(`${GENERATIONS_PREFIX}/`);
1588
+ const genIds = /* @__PURE__ */ new Set();
1589
+ for (const key of allGenKeys) {
1590
+ const id = generationFromKey(key);
1591
+ if (id !== void 0) genIds.add(id);
1592
+ }
1593
+ for await (const key of state.listAsync(`${RECEIPTS_PREFIX}/`)) {
1594
+ const id = generationFromKey(key);
1595
+ if (id !== void 0) genIds.add(id);
1596
+ }
1597
+ const ordered = [...genIds].sort();
1598
+ const retainTail = ordered.slice(-policy.retainGenerations);
1599
+ for (const id of retainTail) protectedGens.add(id);
1600
+ const liveState = new Set(PROTECTED_KEYS);
1601
+ const liveIndex = /* @__PURE__ */ new Set();
1602
+ for (const gen of protectedGens) {
1603
+ const collected = await collectCasAndMapFromHead(state, index, gen);
1604
+ for (const k of collected.stateKeys) liveState.add(k);
1605
+ for (const k of collected.indexKeys) liveIndex.add(k);
1606
+ }
1607
+ const deleteKeys = [];
1608
+ const retainKeys = [];
1609
+ for await (const key of state.listAsync()) {
1610
+ if (PROTECTED_KEYS.has(key)) {
1611
+ retainKeys.push({
1612
+ role: "state",
1613
+ key,
1614
+ reason: "protected-well-known"
1615
+ });
1616
+ continue;
1617
+ }
1618
+ if (liveState.has(key)) {
1619
+ retainKeys.push({
1620
+ role: "state",
1621
+ key,
1622
+ reason: "live"
1623
+ });
1624
+ continue;
1625
+ }
1626
+ const gen = generationFromKey(key);
1627
+ if (gen !== void 0 && protectedGens.has(gen)) {
1628
+ retainKeys.push({
1629
+ role: "state",
1630
+ key,
1631
+ reason: "protected-generation"
1632
+ });
1633
+ continue;
1634
+ }
1635
+ deleteKeys.push({
1636
+ role: "state",
1637
+ key,
1638
+ reason: "unreferenced"
1639
+ });
1640
+ }
1641
+ if (index !== void 0) {
1642
+ for await (const key of index.listAsync()) {
1643
+ if (liveIndex.has(key)) {
1644
+ retainKeys.push({
1645
+ role: "index",
1646
+ key,
1647
+ reason: "live"
1648
+ });
1649
+ continue;
1650
+ }
1651
+ const gen = generationFromKey(key);
1652
+ if (gen !== void 0 && protectedGens.has(gen)) {
1653
+ retainKeys.push({
1654
+ role: "index",
1655
+ key,
1656
+ reason: "protected-generation"
1657
+ });
1658
+ continue;
1659
+ }
1660
+ deleteKeys.push({
1661
+ role: "index",
1662
+ key,
1663
+ reason: "unreferenced"
1664
+ });
1665
+ }
1666
+ }
1667
+ void policy.cdnGraceMs;
1668
+ return {
1669
+ policy,
1670
+ deleteKeys,
1671
+ retainKeys
1672
+ };
1673
+ }
1674
+ async function runGarbageCollection(plan, stores, opts) {
1675
+ const dryRun = opts?.dryRun !== false;
1676
+ const deleted = [];
1677
+ const skipped = [];
1678
+ if (dryRun) {
1679
+ return {
1680
+ dryRun: true,
1681
+ deleted: [],
1682
+ skipped: plan.deleteKeys
1683
+ };
1684
+ }
1685
+ for (const ref of plan.deleteKeys) {
1686
+ try {
1687
+ const store = ref.role === "index" ? stores.index : stores.state;
1688
+ if (store === void 0) {
1689
+ skipped.push({ ...ref, reason: "no-store" });
1690
+ continue;
1691
+ }
1692
+ await store.delete(ref.key);
1693
+ deleted.push(ref);
1694
+ } catch {
1695
+ skipped.push(ref);
297
1696
  }
1697
+ }
1698
+ return {
1699
+ dryRun: false,
1700
+ deleted,
1701
+ skipped
1702
+ };
1703
+ }
1704
+
1705
+ // src/publication/incremental.ts
1706
+ import { randomUUID as randomUUID2 } from "node:crypto";
1707
+
1708
+ // src/publication/shard.ts
1709
+ function shardIdForKey(logicalKey, hexChars = DEFAULT_SHARD_HEX_CHARS) {
1710
+ let h = 2166136261;
1711
+ for (let i = 0; i < logicalKey.length; i++) {
1712
+ h ^= logicalKey.codePointAt(i);
1713
+ h = Math.imul(h, 16777619);
1714
+ }
1715
+ return (h >>> 0).toString(16).padStart(8, "0").slice(0, hexChars);
1716
+ }
1717
+
1718
+ // src/publication/incremental.ts
1719
+ function toBytes3(body) {
1720
+ return typeof body === "string" ? new TextEncoder().encode(body) : body;
1721
+ }
1722
+ async function putCas(store, body, contentType, fenced) {
1723
+ const hash = hashBytes(body);
1724
+ const key = `${CAS_PREFIX}/${hash}`;
1725
+ const opts = {
1726
+ contentType,
1727
+ cacheControl: CACHE_CONTROL_IMMUTABLE
1728
+ };
1729
+ if (fenced) {
1730
+ await putCreateOnly(store, key, body, opts);
1731
+ } else {
1732
+ const existing = await store.get(key);
1733
+ if (existing === void 0) await store.put(key, body, opts);
1734
+ }
1735
+ return { hash, key };
1736
+ }
1737
+ async function loadRoot(state, head) {
1738
+ if (head === void 0 || head.layout !== "incremental-cas") {
1739
+ return { schema: MAP_ROOT_SCHEMA, shards: {} };
1740
+ }
1741
+ const body = await state.get(head.manifestKey);
1742
+ if (body === void 0) {
1743
+ return { schema: MAP_ROOT_SCHEMA, shards: {} };
1744
+ }
1745
+ return JSON.parse(new TextDecoder().decode(body));
1746
+ }
1747
+ async function loadShard(state, shardHash) {
1748
+ if (shardHash === void 0) {
1749
+ return { schema: MAP_SHARD_SCHEMA, entries: {} };
1750
+ }
1751
+ const key = `${MAP_SHARDS_PREFIX}/${shardHash}.json`;
1752
+ const body = await state.get(key);
1753
+ if (body === void 0) {
1754
+ return { schema: MAP_SHARD_SCHEMA, entries: {} };
1755
+ }
1756
+ return JSON.parse(new TextDecoder().decode(body));
1757
+ }
1758
+ async function publishIncremental(input) {
1759
+ const generation = input.generation ?? randomUUID2();
1760
+ const fenced = input.safety.mode !== "unfenced";
1761
+ const hexChars = input.shardHexChars ?? DEFAULT_SHARD_HEX_CHARS;
1762
+ let writes = 0;
1763
+ if (input.assertSafety !== false && input.safety.mode === "conditional-head") {
1764
+ await assertPublicationSafety(input.state, input.safety);
1765
+ }
1766
+ const prior = await readPublishedHead(input.state);
1767
+ const root = await loadRoot(input.state, prior?.head);
1768
+ const touched = /* @__PURE__ */ new Map();
1769
+ const applyChange = async (store, object) => {
1770
+ const logicalKey = object.key.replaceAll(/^\/+/g, "");
1771
+ const bytes = toBytes3(object.body);
1772
+ const contentType = object.contentType ?? "application/json";
1773
+ const cas = await putCas(store, bytes, contentType, fenced);
1774
+ writes += 1;
1775
+ const sid = shardIdForKey(logicalKey, hexChars);
1776
+ let shard = touched.get(sid);
1777
+ if (shard === void 0) {
1778
+ shard = await loadShard(input.state, root.shards[sid]);
1779
+ touched.set(sid, shard);
1780
+ }
1781
+ shard.entries[logicalKey] = {
1782
+ hash: cas.hash,
1783
+ size: bytes.byteLength,
1784
+ contentType
1785
+ };
1786
+ };
1787
+ for (const object of input.indexChanges ?? []) {
1788
+ await applyChange(input.index, object);
1789
+ }
1790
+ for (const object of input.stateChanges ?? []) {
1791
+ await applyChange(input.state, object);
1792
+ }
1793
+ for (const logicalKey of input.deletedKeys ?? []) {
1794
+ const sid = shardIdForKey(logicalKey, hexChars);
1795
+ let shard = touched.get(sid);
1796
+ if (shard === void 0) {
1797
+ shard = await loadShard(input.state, root.shards[sid]);
1798
+ touched.set(sid, shard);
1799
+ }
1800
+ delete shard.entries[logicalKey];
1801
+ }
1802
+ const nextShards = { ...root.shards };
1803
+ for (const [sid, shard] of touched) {
1804
+ const shardBody = `${JSON.stringify(shard, null, 2)}
1805
+ `;
1806
+ const shardHash = hashBytes(shardBody);
1807
+ const shardKey = `${MAP_SHARDS_PREFIX}/${shardHash}.json`;
1808
+ if (fenced) {
1809
+ await putCreateOnly(input.state, shardKey, shardBody, {
1810
+ contentType: "application/json",
1811
+ cacheControl: CACHE_CONTROL_IMMUTABLE
1812
+ });
1813
+ } else {
1814
+ await input.state.put(shardKey, shardBody, {
1815
+ contentType: "application/json",
1816
+ cacheControl: CACHE_CONTROL_IMMUTABLE
1817
+ });
1818
+ }
1819
+ writes += 1;
1820
+ if (Object.keys(shard.entries).length === 0) delete nextShards[sid];
1821
+ else nextShards[sid] = shardHash;
1822
+ }
1823
+ const nextRoot = { schema: MAP_ROOT_SCHEMA, shards: nextShards };
1824
+ const rootBody = `${JSON.stringify(nextRoot, null, 2)}
1825
+ `;
1826
+ const rootHash = hashBytes(rootBody);
1827
+ const rootKey = `${MAP_ROOTS_PREFIX}/${rootHash}.json`;
1828
+ if (fenced) {
1829
+ await putCreateOnly(input.state, rootKey, rootBody, {
1830
+ contentType: "application/json",
1831
+ cacheControl: CACHE_CONTROL_IMMUTABLE
1832
+ });
1833
+ } else {
1834
+ await input.state.put(rootKey, rootBody, {
1835
+ contentType: "application/json",
1836
+ cacheControl: CACHE_CONTROL_IMMUTABLE
1837
+ });
1838
+ }
1839
+ writes += 1;
1840
+ const publishedAt = (/* @__PURE__ */ new Date()).toISOString();
1841
+ const receipt = {
1842
+ schema: RECEIPT_SCHEMA,
1843
+ generation,
1844
+ publishedAt,
1845
+ rootHash,
1846
+ rootKey,
1847
+ changedCount: (input.indexChanges?.length ?? 0) + (input.stateChanges?.length ?? 0) + (input.deletedKeys?.length ?? 0),
1848
+ reducer: {
1849
+ name: input.reducer.name,
1850
+ ...input.reducer.version !== void 0 && { version: input.reducer.version }
1851
+ },
1852
+ ...input.previousGeneration !== void 0 && { previousGeneration: input.previousGeneration },
1853
+ ...prior?.head.generation !== void 0 && input.previousGeneration === void 0 && { previousGeneration: prior.head.generation },
1854
+ ...input.source !== void 0 && { source: input.source }
1855
+ };
1856
+ const receiptBody = `${JSON.stringify(receipt, null, 2)}
1857
+ `;
1858
+ const receiptKey = `${RECEIPTS_PREFIX}/${generation}.json`;
1859
+ if (fenced) {
1860
+ await putCreateOnly(input.state, receiptKey, receiptBody, {
1861
+ contentType: "application/json",
1862
+ cacheControl: CACHE_CONTROL_IMMUTABLE
1863
+ });
1864
+ } else {
1865
+ await input.state.put(receiptKey, receiptBody, {
1866
+ contentType: "application/json",
1867
+ cacheControl: CACHE_CONTROL_IMMUTABLE
1868
+ });
1869
+ }
1870
+ writes += 1;
1871
+ const trustMode = input.trust?.mode ?? "trusted-origin";
1872
+ const head = {
1873
+ schema: HEAD_MANIFEST_SCHEMA,
1874
+ generation,
1875
+ layout: "incremental-cas",
1876
+ manifestKey: rootKey,
1877
+ manifestHash: rootHash,
1878
+ publishedAt,
1879
+ trust: trustMode,
1880
+ ...receipt.previousGeneration !== void 0 && { previousGeneration: receipt.previousGeneration },
1881
+ revision: input.revision ?? (prior?.head.revision ?? 0) + 1
1882
+ };
1883
+ if (input.trust?.mode === "signed") {
1884
+ head.signature = await input.trust.sign(canonicalHeadBytes(head));
1885
+ }
1886
+ const committed = await commitHead(
1887
+ input.state,
1888
+ head,
1889
+ input.safety,
1890
+ fenced,
1891
+ input.expectedHeadEtag ?? prior?.headEtag
1892
+ );
1893
+ if ("conflict" in committed) {
1894
+ return {
1895
+ ok: false,
1896
+ conflict: {
1897
+ kind: committed.conflict.kind,
1898
+ message: committed.conflict.message
1899
+ }
1900
+ };
1901
+ }
1902
+ void CACHE_CONTROL_REVALIDATE;
1903
+ void HEAD_KEY;
1904
+ return {
1905
+ ok: true,
1906
+ generation,
1907
+ head,
1908
+ receipt,
1909
+ root: nextRoot,
1910
+ rootHash,
1911
+ writes,
1912
+ ...committed.etag !== void 0 && { headEtag: committed.etag }
298
1913
  };
299
1914
  }
300
1915
 
@@ -306,13 +1921,60 @@ var noopReducer = {
306
1921
  };
307
1922
  export {
308
1923
  AbstractActor,
1924
+ CACHE_CONTROL_IMMUTABLE,
1925
+ CACHE_CONTROL_REVALIDATE,
1926
+ CAS_PREFIX,
1927
+ ConditionalWriteError,
1928
+ DAPP_STORE_ROLES,
1929
+ DEFAULT_SHARD_HEX_CHARS,
309
1930
  DappActor,
310
1931
  DappDataStoreMoniker,
311
1932
  DappIndexStoreMoniker,
312
1933
  DappStateStoreMoniker,
1934
+ GENERATIONS_PREFIX,
1935
+ GENERATION_MANIFEST_SCHEMA,
1936
+ HEAD_KEY,
1937
+ HEAD_MANIFEST_SCHEMA,
1938
+ INDEXER_STATUS_SCHEMA,
1939
+ MAP_ROOTS_PREFIX,
1940
+ MAP_ROOT_SCHEMA,
1941
+ MAP_SHARDS_PREFIX,
1942
+ MAP_SHARD_SCHEMA,
1943
+ PINS_KEY,
1944
+ PINS_SCHEMA,
1945
+ RECEIPTS_PREFIX,
1946
+ RECEIPT_SCHEMA,
1947
+ ReadOnlyStoreError,
1948
+ STATUS_KEY,
1949
+ asReadOnly,
1950
+ asReadOnlyWriter,
1951
+ assertPublicationSafety,
313
1952
  bootDappActors,
1953
+ canonicalHeadBytes,
314
1954
  createDappLocator,
1955
+ createMemoryBucketStore,
315
1956
  createS3BucketStore,
316
- noopReducer
1957
+ hashBytes,
1958
+ isConditionalWriteError,
1959
+ joinKey,
1960
+ listPins,
1961
+ listReleases,
1962
+ noopReducer,
1963
+ normalizePublicBaseUrl,
1964
+ pinRelease,
1965
+ planGarbageCollection,
1966
+ probePublicationCapabilities,
1967
+ publishGeneration,
1968
+ publishIncremental,
1969
+ putCreateOnly,
1970
+ readIndexerStatus,
1971
+ readPublishedHead,
1972
+ resolveS3Client,
1973
+ rollbackHead,
1974
+ runGarbageCollection,
1975
+ shardIdForKey,
1976
+ stripPrefix,
1977
+ unpinRelease,
1978
+ writeIndexerStatus
317
1979
  };
318
1980
  //# sourceMappingURL=index.mjs.map