@ariestools/aries-dapp-core 0.1.7 → 0.1.9
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.
- package/README.md +326 -32
- package/dist/bin/dappServer.mjs +685 -104
- package/dist/node/DappActor.d.ts +28 -7
- package/dist/node/DappActor.d.ts.map +1 -1
- package/dist/node/actor/AbstractActor.d.ts +13 -8
- package/dist/node/actor/AbstractActor.d.ts.map +1 -1
- package/dist/node/actor/types.d.ts +7 -0
- package/dist/node/actor/types.d.ts.map +1 -1
- package/dist/node/bin/dappServer.d.ts +11 -4
- package/dist/node/bin/dappServer.d.ts.map +1 -1
- package/dist/node/bin/loadReducer.d.ts +17 -0
- package/dist/node/bin/loadReducer.d.ts.map +1 -0
- package/dist/node/boot/bootDappActors.d.ts +12 -1
- package/dist/node/boot/bootDappActors.d.ts.map +1 -1
- package/dist/node/consumer/api.d.ts +56 -0
- package/dist/node/consumer/api.d.ts.map +1 -0
- package/dist/node/consumer/hash.d.ts +6 -0
- package/dist/node/consumer/hash.d.ts.map +1 -0
- package/dist/node/consumer/parse.d.ts +18 -0
- package/dist/node/consumer/parse.d.ts.map +1 -0
- package/dist/node/consumer.d.ts +9 -0
- package/dist/node/consumer.d.ts.map +1 -0
- package/dist/node/consumer.mjs +330 -0
- package/dist/node/consumer.mjs.map +7 -0
- package/dist/node/examples/productionComposition.d.ts +118 -0
- package/dist/node/examples/productionComposition.d.ts.map +1 -0
- package/dist/node/index.d.ts +25 -3
- package/dist/node/index.d.ts.map +1 -1
- package/dist/node/index.mjs +1730 -68
- package/dist/node/index.mjs.map +4 -4
- package/dist/node/locator/createDappLocator.d.ts +32 -5
- package/dist/node/locator/createDappLocator.d.ts.map +1 -1
- package/dist/node/providers/DappBucketStore.d.ts +26 -21
- package/dist/node/providers/DappBucketStore.d.ts.map +1 -1
- package/dist/node/providers/capabilities.d.ts +49 -0
- package/dist/node/providers/capabilities.d.ts.map +1 -0
- package/dist/node/providers/errors.d.ts +11 -0
- package/dist/node/providers/errors.d.ts.map +1 -0
- package/dist/node/providers/hashBytes.d.ts +3 -0
- package/dist/node/providers/hashBytes.d.ts.map +1 -0
- package/dist/node/providers/joinKey.d.ts +11 -0
- package/dist/node/providers/joinKey.d.ts.map +1 -0
- package/dist/node/providers/memoryBucketStore.d.ts +14 -0
- package/dist/node/providers/memoryBucketStore.d.ts.map +1 -0
- package/dist/node/providers/types.d.ts +99 -0
- package/dist/node/providers/types.d.ts.map +1 -0
- package/dist/node/publication/canonicalHead.d.ts +7 -0
- package/dist/node/publication/canonicalHead.d.ts.map +1 -0
- package/dist/node/publication/commitHead.d.ts +9 -0
- package/dist/node/publication/commitHead.d.ts.map +1 -0
- package/dist/node/publication/constants.d.ts +27 -0
- package/dist/node/publication/constants.d.ts.map +1 -0
- package/dist/node/publication/gc.d.ts +42 -0
- package/dist/node/publication/gc.d.ts.map +1 -0
- package/dist/node/publication/incremental.d.ts +83 -0
- package/dist/node/publication/incremental.d.ts.map +1 -0
- package/dist/node/publication/publishGeneration.d.ts +18 -0
- package/dist/node/publication/publishGeneration.d.ts.map +1 -0
- package/dist/node/publication/putCreateOnly.d.ts +11 -0
- package/dist/node/publication/putCreateOnly.d.ts.map +1 -0
- package/dist/node/publication/releases.d.ts +49 -0
- package/dist/node/publication/releases.d.ts.map +1 -0
- package/dist/node/publication/safety.d.ts +55 -0
- package/dist/node/publication/safety.d.ts.map +1 -0
- package/dist/node/publication/shard.d.ts +7 -0
- package/dist/node/publication/shard.d.ts.map +1 -0
- package/dist/node/publication/status.d.ts +27 -0
- package/dist/node/publication/status.d.ts.map +1 -0
- package/dist/node/publication/statusPatch.d.ts +10 -0
- package/dist/node/publication/statusPatch.d.ts.map +1 -0
- package/dist/node/publication/types.d.ts +138 -0
- package/dist/node/publication/types.d.ts.map +1 -0
- package/dist/node/publication/writeGenerationObjects.d.ts +5 -0
- package/dist/node/publication/writeGenerationObjects.d.ts.map +1 -0
- package/dist/node/reducer/DappReducer.d.ts +37 -14
- package/dist/node/reducer/DappReducer.d.ts.map +1 -1
- package/examples/countFactsReducer.mjs +96 -0
- package/package.json +16 -8
package/dist/node/index.mjs
CHANGED
|
@@ -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
|
|
68
|
-
*
|
|
69
|
-
*
|
|
70
|
-
*
|
|
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"
|
|
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.
|
|
108
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
374
|
+
this.registerTimer(REDUCE_TIMER, async () => {
|
|
167
375
|
await this.runReducePass();
|
|
168
376
|
}, firstRunDelay, interval);
|
|
169
|
-
this.logger.info(
|
|
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
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
193
|
-
const
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
538
|
+
const status = error.$metadata?.httpStatusCode;
|
|
539
|
+
return name === "NoSuchKey" || name === "NotFound" || name === "404" || status === 404;
|
|
220
540
|
}
|
|
221
|
-
function
|
|
222
|
-
const
|
|
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:
|
|
226
|
-
forcePathStyle:
|
|
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({
|
|
237
|
-
|
|
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
|
|
244
|
-
|
|
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
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
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
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
const
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
-
|
|
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
|