@takboo/dsh-session-bin 0.0.0-stage → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +122 -2
  3. package/cordis.patch.yml +4 -0
  4. package/dist/client-model.js +1673 -0
  5. package/dist/client.js +17043 -0
  6. package/dist/client.js.map +1 -0
  7. package/dist/host/archive.d.ts +766 -0
  8. package/dist/host/lease.d.ts +4 -0
  9. package/dist/host/module.d.ts +705 -0
  10. package/dist/host/native-jsonl-files.d.ts +198 -0
  11. package/dist/host/native-retirement-admission.d.ts +33 -0
  12. package/dist/host/native-retirement-metadata.d.ts +116 -0
  13. package/dist/host/native-retirement-owner.d.ts +305 -0
  14. package/dist/host/native-retirement-persistence.d.ts +24 -0
  15. package/dist/host/native-retirement-sdk.d.ts +80 -0
  16. package/dist/host/native.d.ts +12 -0
  17. package/dist/host/platform-files.d.ts +19 -0
  18. package/dist/host/remote.d.ts +32 -0
  19. package/dist/host/retirement-owner-store.d.ts +124 -0
  20. package/dist/host/retirement-owner.d.ts +124 -0
  21. package/dist/host/retirement-store.d.ts +311 -0
  22. package/dist/host/retirement.d.ts +76 -0
  23. package/dist/host/store.d.ts +215 -0
  24. package/dist/index.d.ts +912 -0
  25. package/dist/index.js +5008 -0
  26. package/dist/index.js.map +7 -0
  27. package/dist/operations/archive.d.ts +82 -0
  28. package/dist/operations/index.d.ts +2 -0
  29. package/dist/operations/retirement-owner.d.ts +129 -0
  30. package/dist/operations/retirement.d.ts +821 -0
  31. package/dist/operations/schema.d.ts +115 -0
  32. package/dist/operations.js +130 -0
  33. package/dist/operations.js.map +7 -0
  34. package/dist/remote/contracts.d.ts +891 -0
  35. package/dist/remote.js +442 -0
  36. package/dist/remote.js.map +7 -0
  37. package/dist/types/client/components.d.ts +17 -0
  38. package/dist/types/client/index.d.ts +10 -0
  39. package/dist/types/client/locales.d.ts +116 -0
  40. package/dist/types/client/model.d.ts +137 -0
  41. package/dist/types/client/purge-batch.d.ts +45 -0
  42. package/dist/types/client/retirement-digest.d.ts +3 -0
  43. package/dist/types/operations/archive.d.ts +82 -0
  44. package/dist/types/operations/retirement.d.ts +821 -0
  45. package/dist/types/operations/schema.d.ts +115 -0
  46. package/dist/types/remote/contracts.d.ts +891 -0
  47. package/docs/client-interface.md +79 -0
  48. package/docs/host-lifecycle.md +160 -0
  49. package/locale/en.json +6 -0
  50. package/locale/zh.json +6 -0
  51. package/package.json +163 -4
@@ -0,0 +1,1673 @@
1
+ // src/operations/schema.ts
2
+ import { z } from "zod";
3
+ var identity = z.string().min(1).max(1024);
4
+ var actionSchema = z.enum(["bin", "restore"]);
5
+ var blockerSchema = z.object({
6
+ code: z.string().min(1),
7
+ activity: z.array(z.string()).optional()
8
+ }).strict();
9
+ var entrySchema = z.object({
10
+ schemaVersion: z.literal(1),
11
+ sessionId: identity,
12
+ entryId: z.uuid(),
13
+ operationId: identity,
14
+ binnedAt: z.iso.datetime(),
15
+ workspaceIdAtBin: identity.nullable(),
16
+ wasArchived: z.boolean()
17
+ }).strict();
18
+ var planSchema = z.object({
19
+ schemaVersion: z.literal(1),
20
+ operationId: identity,
21
+ action: actionSchema,
22
+ sessionId: identity,
23
+ expected: z.object({ archived: z.boolean(), entryId: z.uuid().nullable() }).strict(),
24
+ blockers: z.array(blockerSchema)
25
+ }).strict();
26
+ var resultSchema = z.object({
27
+ operationId: identity,
28
+ action: actionSchema,
29
+ sessionId: identity,
30
+ status: z.enum(["success", "rejected", "conflict"]),
31
+ reason: z.string().min(1).nullable(),
32
+ entryId: z.uuid().nullable()
33
+ }).strict();
34
+ var operationSchema = z.object({
35
+ schemaVersion: z.literal(1),
36
+ plan: planSchema,
37
+ createdAt: z.iso.datetime(),
38
+ phase: z.enum(["intent", "applied", "done"]),
39
+ ownershipInvalidated: z.boolean(),
40
+ entry: entrySchema.nullable(),
41
+ result: resultSchema.nullable()
42
+ }).strict().superRefine((operation, ctx) => {
43
+ const { plan, entry, result, phase } = operation;
44
+ if (phase === "done" !== (result !== null)) {
45
+ ctx.addIssue({ code: "custom", message: "Only a done operation has a result." });
46
+ }
47
+ if (phase !== "done" && entry === null) {
48
+ ctx.addIssue({ code: "custom", message: "An unfinished operation requires its entry snapshot." });
49
+ }
50
+ if (entry && (entry.sessionId !== plan.sessionId || plan.action === "bin" && (entry.operationId !== plan.operationId || entry.wasArchived !== plan.expected.archived) || plan.action === "restore" && entry.entryId !== plan.expected.entryId)) {
51
+ ctx.addIssue({ code: "custom", message: "Entry snapshot does not match the operation." });
52
+ }
53
+ if (result && (result.status === "success" !== (result.reason === null) || result.entryId !== (entry?.entryId ?? null) || result.status === "success" && !entry)) {
54
+ ctx.addIssue({ code: "custom", message: "Result outcome or entry snapshot is inconsistent." });
55
+ }
56
+ if (operation.ownershipInvalidated && !entry) {
57
+ ctx.addIssue({ code: "custom", message: "Ownership invalidation requires an entry identity." });
58
+ }
59
+ if (result && (result.operationId !== plan.operationId || result.sessionId !== plan.sessionId || result.action !== plan.action)) {
60
+ ctx.addIssue({ code: "custom", message: "Result does not match the operation." });
61
+ }
62
+ });
63
+ var prepareRequestSchema = z.object({
64
+ action: actionSchema,
65
+ sessionId: identity,
66
+ operationId: identity.optional()
67
+ }).strict();
68
+
69
+ // src/operations/archive.ts
70
+ import { z as z2 } from "zod";
71
+ var identity2 = z2.string().min(1).max(1024);
72
+ var archiveEntrySchema = z2.object({
73
+ schemaVersion: z2.literal(2),
74
+ sessionId: identity2,
75
+ entryId: z2.uuid()
76
+ }).strict();
77
+ var archivePlanSchema = z2.object({
78
+ schemaVersion: z2.literal(2),
79
+ operationId: identity2,
80
+ action: z2.literal("unarchive"),
81
+ sessionId: identity2,
82
+ expected: z2.object({ archived: z2.boolean(), entryId: z2.uuid().nullable() }).strict(),
83
+ blockers: z2.array(blockerSchema)
84
+ }).strict();
85
+ var archiveResultSchema = z2.object({
86
+ operationId: identity2,
87
+ action: z2.literal("unarchive"),
88
+ sessionId: identity2,
89
+ status: z2.enum(["success", "rejected", "conflict"]),
90
+ reason: z2.string().min(1).nullable(),
91
+ entryId: z2.uuid().nullable()
92
+ }).strict();
93
+ var archiveOperationSchema = z2.object({
94
+ schemaVersion: z2.literal(2),
95
+ plan: archivePlanSchema,
96
+ createdAt: z2.iso.datetime(),
97
+ phase: z2.enum(["intent", "applied", "done"]),
98
+ entry: archiveEntrySchema.nullable(),
99
+ result: archiveResultSchema.nullable()
100
+ }).strict().superRefine(({ plan, entry, result, phase }, ctx) => {
101
+ if (phase === "done" !== (result !== null) || phase !== "done" && (entry === null || !plan.expected.archived)) {
102
+ ctx.addIssue({ code: "custom", message: "Only done operations have results; unfinished operations require an entry." });
103
+ }
104
+ if (entry && (entry.sessionId !== plan.sessionId || entry.entryId !== plan.expected.entryId)) {
105
+ ctx.addIssue({ code: "custom", message: "Entry snapshot does not match the plan." });
106
+ }
107
+ if (result && (result.operationId !== plan.operationId || result.sessionId !== plan.sessionId || result.action !== plan.action || result.entryId !== (entry?.entryId ?? null) || result.status === "success" !== (result.reason === null) || result.status === "success" && !entry)) {
108
+ ctx.addIssue({ code: "custom", message: "Result does not match the operation." });
109
+ }
110
+ });
111
+ var archivePrepareRequestSchema = z2.object({
112
+ action: z2.literal("unarchive"),
113
+ sessionId: identity2,
114
+ operationId: identity2.optional()
115
+ }).strict();
116
+
117
+ // src/remote/contracts.ts
118
+ import { z as z4 } from "zod";
119
+
120
+ // src/operations/retirement.ts
121
+ import { z as z3 } from "zod";
122
+ var key = z3.string().min(1).max(1024);
123
+ var lifecycleKeySchema = z3.object({ storeId: key, sessionId: key, lifecycleId: key }).strict();
124
+ var retirementParticipantSchema = z3.object({ id: key, version: key }).strict();
125
+ var retirementCapabilitiesSchema = z3.object({
126
+ protocolVersion: z3.literal(1),
127
+ ownerId: key,
128
+ hostVersion: key,
129
+ providerId: key,
130
+ storeId: key,
131
+ participants: z3.array(retirementParticipantSchema).min(1).max(64)
132
+ }).strict().superRefine((value2, ctx) => {
133
+ if (new Set(value2.participants.map((item) => item.id)).size !== value2.participants.length) {
134
+ ctx.addIssue({ code: "custom", message: "Participant identities must be unique." });
135
+ }
136
+ });
137
+ var legacyRetirementBindingSchema = z3.object({
138
+ schemaVersion: z3.literal(1),
139
+ entryId: z3.uuid(),
140
+ entryVersion: z3.literal(1),
141
+ lifecycle: lifecycleKeySchema,
142
+ capabilities: retirementCapabilitiesSchema
143
+ }).strict();
144
+ var nativeRetirementBindingSchema = z3.object({
145
+ schemaVersion: z3.literal(2),
146
+ target: z3.literal("native-archive"),
147
+ entryId: z3.uuid(),
148
+ entryVersion: z3.literal(2),
149
+ lifecycle: lifecycleKeySchema,
150
+ capabilities: retirementCapabilitiesSchema
151
+ }).strict();
152
+ var retirementBindingSchema = z3.union([legacyRetirementBindingSchema, nativeRetirementBindingSchema]).superRefine((value2, ctx) => {
153
+ if (value2.lifecycle.storeId !== value2.capabilities.storeId) {
154
+ ctx.addIssue({ code: "custom", message: "Binding store differs from its owner." });
155
+ }
156
+ });
157
+ var retirementResourceSchema = z3.object({
158
+ ownerId: key,
159
+ resourceId: key,
160
+ revision: key,
161
+ kind: z3.enum(["transcript", "workspace", "index", "cache", "attachment", "spill", "coordination"]),
162
+ disposition: z3.enum(["erase", "release-reference", "retain-shared", "retain-coordination"]),
163
+ retention: z3.object({
164
+ reason: z3.enum(["shared-reference", "coordination-identity"]),
165
+ retainedBy: z3.array(lifecycleKeySchema).max(4096)
166
+ }).strict().nullable()
167
+ }).strict();
168
+ var retirementManifestSchema = z3.object({
169
+ schemaVersion: z3.literal(1),
170
+ lifecycle: lifecycleKeySchema,
171
+ capabilities: retirementCapabilitiesSchema,
172
+ resources: z3.array(retirementResourceSchema).min(1).max(4096)
173
+ }).strict().superRefine((value2, ctx) => {
174
+ const owners = new Set(value2.capabilities.participants.map((item) => item.id));
175
+ const resourceKeys = value2.resources.map((item) => JSON.stringify([item.ownerId, item.resourceId]));
176
+ if (new Set(resourceKeys).size !== resourceKeys.length || value2.resources.some((item) => !owners.has(item.ownerId))) {
177
+ ctx.addIssue({ code: "custom", message: "Resources require unique keys and declared owners." });
178
+ }
179
+ if (value2.lifecycle.storeId !== value2.capabilities.storeId || !value2.resources.some((item) => item.kind === "transcript" && item.disposition === "erase")) {
180
+ ctx.addIssue({ code: "custom", message: "Manifest must identify its store and erase the target transcript." });
181
+ }
182
+ for (const resource of value2.resources) {
183
+ if (resource.kind === "coordination" && resource.disposition !== "retain-coordination") {
184
+ ctx.addIssue({ code: "custom", message: "Stable coordination identity must be retained." });
185
+ }
186
+ const retained = resource.retention;
187
+ if (resource.disposition === "retain-shared") {
188
+ if (!retained || retained.reason !== "shared-reference" || !retained.retainedBy.length || retained.retainedBy.some((key2) => lifecycleEqual(key2, value2.lifecycle)) || new Set(retained.retainedBy.map((key2) => JSON.stringify([key2.storeId, key2.sessionId, key2.lifecycleId]))).size !== retained.retainedBy.length) {
189
+ ctx.addIssue({ code: "custom", message: "Shared retention requires distinct surviving lifecycle witnesses." });
190
+ }
191
+ } else if (resource.disposition === "retain-coordination") {
192
+ if (resource.kind !== "coordination" || !retained || retained.reason !== "coordination-identity" || retained.retainedBy.length) {
193
+ ctx.addIssue({ code: "custom", message: "Coordination retention must state its identity purpose." });
194
+ }
195
+ } else if (retained !== null) {
196
+ ctx.addIssue({ code: "custom", message: "Erasure/reference release does not retain this resource." });
197
+ }
198
+ }
199
+ });
200
+ var retirementRequestSchema = z3.object({
201
+ operationId: key,
202
+ expected: lifecycleKeySchema,
203
+ bin: z3.union([
204
+ z3.object({ entryId: z3.uuid(), entryVersion: z3.literal(1) }).strict(),
205
+ z3.object({ kind: z3.literal("native-archive"), entryId: z3.uuid(), entryVersion: z3.literal(2) }).strict()
206
+ ]),
207
+ manifestDigest: z3.string().regex(/^[a-f0-9]{64}$/)
208
+ }).strict();
209
+ var retirementResourceReceiptSchema = z3.object({
210
+ ownerId: key,
211
+ resourceId: key,
212
+ status: z3.enum(["erased", "reference-released", "retained", "failed"]),
213
+ reason: key.nullable()
214
+ }).strict().superRefine((value2, ctx) => {
215
+ if ((value2.status === "failed" || value2.status === "retained") !== (value2.reason !== null)) {
216
+ ctx.addIssue({ code: "custom", message: "Failure and retention require a reason; erased/released resources do not." });
217
+ }
218
+ });
219
+ var retirementAuthorizationSchema = z3.discriminatedUnion("authorized", [
220
+ z3.object({ authorized: z3.literal(true), authorizationId: z3.uuid() }).strict(),
221
+ z3.object({ authorized: z3.literal(false), reason: key }).strict()
222
+ ]);
223
+ var retirementStateSchema = z3.object({
224
+ schemaVersion: z3.literal(1),
225
+ request: retirementRequestSchema,
226
+ manifest: retirementManifestSchema,
227
+ authorizationId: z3.uuid().nullable(),
228
+ phase: z3.enum(["rejected", "conflict", "fenced", "quiesced", "erasing", "converging", "done"]),
229
+ reason: key.nullable(),
230
+ resources: z3.array(retirementResourceReceiptSchema).max(4096)
231
+ }).strict().superRefine((value2, ctx) => {
232
+ if (!lifecycleEqual(value2.request.expected, value2.manifest.lifecycle)) {
233
+ ctx.addIssue({ code: "custom", message: "Owner state changed the requested lifecycle." });
234
+ }
235
+ const refused = value2.phase === "rejected" || value2.phase === "conflict";
236
+ if (refused !== (value2.authorizationId === null)) {
237
+ ctx.addIssue({ code: "custom", message: "Admitted owner state requires its handed-off authorization grant." });
238
+ }
239
+ if (refused !== (value2.reason !== null)) {
240
+ ctx.addIssue({ code: "custom", message: "Admission refusal requires a reason." });
241
+ }
242
+ const keys = value2.resources.map((item) => JSON.stringify([item.ownerId, item.resourceId]));
243
+ if (new Set(keys).size !== keys.length) ctx.addIssue({ code: "custom", message: "Duplicate resource receipts." });
244
+ for (const receipt of value2.resources) {
245
+ const resource = value2.manifest.resources.find((item) => item.ownerId === receipt.ownerId && item.resourceId === receipt.resourceId);
246
+ if (!resource) ctx.addIssue({ code: "custom", message: "Receipt is outside the frozen manifest." });
247
+ if (resource && receipt.status !== "failed") {
248
+ const expected = resource.disposition === "erase" ? "erased" : resource.disposition === "release-reference" ? "reference-released" : "retained";
249
+ if (receipt.status !== expected || receipt.status === "retained" && receipt.reason !== resource.retention?.reason) {
250
+ ctx.addIssue({ code: "custom", message: "Resource acknowledgement did not satisfy its frozen disposition and retention reason." });
251
+ }
252
+ }
253
+ }
254
+ if (value2.phase === "done" && (value2.resources.length !== value2.manifest.resources.length || value2.resources.some((receipt) => receipt.status === "failed"))) {
255
+ ctx.addIssue({ code: "custom", message: "Completion requires every resource receipt without failures." });
256
+ }
257
+ if (["rejected", "conflict", "fenced", "quiesced"].includes(value2.phase) && value2.resources.length) {
258
+ ctx.addIssue({ code: "custom", message: "Admission and quiescence phases cannot include resource erasure effects." });
259
+ }
260
+ });
261
+ var preparePurgeRequestSchema = z3.object({ sessionId: key, operationId: key.optional() }).strict();
262
+ var purgePlanSchema = z3.object({
263
+ schemaVersion: z3.union([z3.literal(1), z3.literal(2)]),
264
+ action: z3.literal("purge"),
265
+ operationId: key,
266
+ sessionId: key,
267
+ expectedEntryId: z3.uuid().nullable(),
268
+ binding: retirementBindingSchema.nullable(),
269
+ manifest: retirementManifestSchema.nullable(),
270
+ blockers: z3.array(blockerSchema)
271
+ }).strict().superRefine((value2, ctx) => {
272
+ if (value2.binding && value2.binding.schemaVersion !== value2.schemaVersion) {
273
+ ctx.addIssue({ code: "custom", message: "Legacy Bin and native archive deletion bindings cannot be interchanged." });
274
+ }
275
+ if (value2.binding && (value2.binding.entryId !== value2.expectedEntryId || value2.binding.lifecycle.sessionId !== value2.sessionId)) {
276
+ ctx.addIssue({ code: "custom", message: "Purge binding differs from its target entry." });
277
+ }
278
+ if (value2.manifest && (!value2.binding || !lifecycleEqual(value2.binding.lifecycle, value2.manifest.lifecycle))) {
279
+ ctx.addIssue({ code: "custom", message: "Purge manifest differs from its bound lifecycle." });
280
+ }
281
+ });
282
+ var purgeResultSchema = z3.object({
283
+ action: z3.literal("purge"),
284
+ operationId: key,
285
+ sessionId: key,
286
+ entryId: z3.uuid().nullable(),
287
+ status: z3.enum(["success", "rejected", "conflict", "pending-recovery", "partial-failure"]),
288
+ reason: key.nullable(),
289
+ ownerState: retirementStateSchema.nullable()
290
+ }).strict().superRefine((value2, ctx) => {
291
+ if (value2.status === "success" !== (value2.reason === null) || value2.status === "success" && value2.ownerState?.phase !== "done") {
292
+ ctx.addIssue({ code: "custom", message: "Success requires a complete owner receipt." });
293
+ }
294
+ if (value2.ownerState && (value2.ownerState.request.operationId !== value2.operationId || value2.ownerState.request.expected.sessionId !== value2.sessionId || value2.ownerState.request.bin.entryId !== value2.entryId)) {
295
+ ctx.addIssue({ code: "custom", message: "Purge result differs from its owner receipt." });
296
+ }
297
+ });
298
+ var purgeOperationSchema = z3.object({
299
+ schemaVersion: z3.union([z3.literal(1), z3.literal(2)]),
300
+ plan: purgePlanSchema,
301
+ createdAt: z3.iso.datetime(),
302
+ phase: z3.enum(["intent", "authorizing", "owner-pending", "done"]),
303
+ entry: z3.union([entrySchema, archiveEntrySchema]).nullable(),
304
+ authorizationId: z3.uuid().nullable(),
305
+ ownerState: retirementStateSchema.nullable(),
306
+ result: purgeResultSchema.nullable()
307
+ }).strict().superRefine((value2, ctx) => {
308
+ const { plan, entry, result, ownerState } = value2;
309
+ if (value2.schemaVersion !== plan.schemaVersion || entry && entry.schemaVersion !== plan.schemaVersion || ownerState && ownerState.request.bin.entryVersion !== plan.schemaVersion) {
310
+ ctx.addIssue({ code: "custom", message: "Deletion journal target versions must agree." });
311
+ }
312
+ if (entry && (entry.entryId !== plan.expectedEntryId || entry.sessionId !== plan.sessionId)) {
313
+ ctx.addIssue({ code: "custom", message: "Purge journal entry differs from the plan." });
314
+ }
315
+ if (value2.phase !== "done" && (!entry || !plan.binding || !plan.manifest)) {
316
+ ctx.addIssue({ code: "custom", message: "Unfinished purge requires an entry, binding and manifest." });
317
+ }
318
+ const terminalResult = result && ["success", "rejected", "conflict"].includes(result.status);
319
+ if (value2.phase === "done" !== Boolean(terminalResult) || ["intent", "authorizing"].includes(value2.phase) && (ownerState !== null || result !== null) || value2.phase === "intent" && value2.authorizationId !== null || value2.phase === "authorizing" && value2.authorizationId === null || ownerState && ownerState.authorizationId !== null && ownerState.authorizationId !== value2.authorizationId || value2.phase === "owner-pending" && (ownerState === null || result === null)) {
320
+ ctx.addIssue({ code: "custom", message: "Purge journal phase and receipt are inconsistent." });
321
+ }
322
+ if (result && (result.operationId !== plan.operationId || result.sessionId !== plan.sessionId || result.entryId !== (entry?.entryId ?? null) || JSON.stringify(result.ownerState) !== JSON.stringify(ownerState))) {
323
+ ctx.addIssue({ code: "custom", message: "Purge journal result differs from its saved state." });
324
+ }
325
+ });
326
+ function lifecycleEqual(a, b) {
327
+ return a.storeId === b.storeId && a.sessionId === b.sessionId && a.lifecycleId === b.lifecycleId;
328
+ }
329
+
330
+ // src/remote/contracts.ts
331
+ var sessionBinRemoteNamespace = "sessionBin";
332
+ var sessionBinRemoteServiceKey = "sessionBinRemote";
333
+ var snapshotSchema = z4.object({ schemaVersion: z4.literal(2), entries: z4.array(archiveEntrySchema) }).strict();
334
+ var operationIdSchema = z4.string().min(1).max(1024);
335
+ var entriesSchema = z4.array(archiveEntrySchema);
336
+ var optionalOperationSchema = z4.union([archiveOperationSchema, operationSchema]).nullable();
337
+ var nativePurgePlanSchema = purgePlanSchema.refine((plan) => plan.schemaVersion === 2, "Only native archive v2 deletion plans may be executed remotely.");
338
+ var optionalPurgeOperationSchema = purgeOperationSchema.nullable();
339
+ var purgeOperationsSchema = z4.array(purgeOperationSchema);
340
+ function strictCodec(name, schema) {
341
+ return {
342
+ mode: "strict",
343
+ typeSymbol: `@takboo/dsh-session-bin#${name}`,
344
+ create: () => schema,
345
+ encode: (value2) => schema.parse(value2),
346
+ decode: (value2) => schema.parse(value2)
347
+ };
348
+ }
349
+ var codecs = {
350
+ request: strictCodec("ArchivePrepareRequest", archivePrepareRequestSchema),
351
+ plan: strictCodec("ArchivePlan", archivePlanSchema),
352
+ result: strictCodec("ArchiveResult", archiveResultSchema),
353
+ entries: strictCodec("ArchiveEntries", entriesSchema),
354
+ operationId: strictCodec("OperationId", operationIdSchema),
355
+ operation: strictCodec("OptionalArchiveOperation", optionalOperationSchema),
356
+ purgeRequest: strictCodec("PreparePurgeRequest", preparePurgeRequestSchema),
357
+ purgePlan: strictCodec("NativePurgePlan", nativePurgePlanSchema),
358
+ purgeResult: strictCodec("PurgeResult", purgeResultSchema),
359
+ purgeOperation: strictCodec("OptionalPurgeOperation", optionalPurgeOperationSchema),
360
+ purgeOperations: strictCodec("PurgeOperations", purgeOperationsSchema),
361
+ snapshot: strictCodec("ArchiveSnapshot", snapshotSchema)
362
+ };
363
+ function descriptor(method, parameters, result, mode) {
364
+ return {
365
+ id: `@takboo/dsh-session-bin#sessionBin/${method}`,
366
+ service: sessionBinRemoteServiceKey,
367
+ namespace: sessionBinRemoteNamespace,
368
+ method,
369
+ invocation: { kind: "direct" },
370
+ parameters,
371
+ result,
372
+ cancellation: { parameter: "signal" },
373
+ ...mode === void 0 ? {} : { mode }
374
+ };
375
+ }
376
+ var descriptors = [
377
+ descriptor("prepare", [{ name: "request", wire: "request", source: "json", codec: codecs.request }], codecs.plan),
378
+ descriptor("execute", [{ name: "plan", wire: "plan", source: "json", codec: codecs.plan }], codecs.result),
379
+ descriptor("list", [], codecs.entries),
380
+ descriptor("getOperation", [{ name: "operationId", wire: "operationId", source: "json", codec: codecs.operationId }], codecs.operation),
381
+ descriptor("preparePurge", [{ name: "request", wire: "request", source: "json", codec: codecs.purgeRequest }], codecs.purgePlan),
382
+ descriptor("executePurge", [{ name: "plan", wire: "plan", source: "json", codec: codecs.purgePlan }], codecs.purgeResult),
383
+ descriptor("getPurgeOperation", [{ name: "operationId", wire: "operationId", source: "json", codec: codecs.operationId }], codecs.purgeOperation),
384
+ descriptor("purgeOperations", [], codecs.purgeOperations),
385
+ descriptor("follow", [], codecs.snapshot, "stream")
386
+ ];
387
+
388
+ // src/client/retirement-digest.ts
389
+ var SHA256_CONSTANTS = new Uint32Array([
390
+ 1116352408,
391
+ 1899447441,
392
+ 3049323471,
393
+ 3921009573,
394
+ 961987163,
395
+ 1508970993,
396
+ 2453635748,
397
+ 2870763221,
398
+ 3624381080,
399
+ 310598401,
400
+ 607225278,
401
+ 1426881987,
402
+ 1925078388,
403
+ 2162078206,
404
+ 2614888103,
405
+ 3248222580,
406
+ 3835390401,
407
+ 4022224774,
408
+ 264347078,
409
+ 604807628,
410
+ 770255983,
411
+ 1249150122,
412
+ 1555081692,
413
+ 1996064986,
414
+ 2554220882,
415
+ 2821834349,
416
+ 2952996808,
417
+ 3210313671,
418
+ 3336571891,
419
+ 3584528711,
420
+ 113926993,
421
+ 338241895,
422
+ 666307205,
423
+ 773529912,
424
+ 1294757372,
425
+ 1396182291,
426
+ 1695183700,
427
+ 1986661051,
428
+ 2177026350,
429
+ 2456956037,
430
+ 2730485921,
431
+ 2820302411,
432
+ 3259730800,
433
+ 3345764771,
434
+ 3516065817,
435
+ 3600352804,
436
+ 4094571909,
437
+ 275423344,
438
+ 430227734,
439
+ 506948616,
440
+ 659060556,
441
+ 883997877,
442
+ 958139571,
443
+ 1322822218,
444
+ 1537002063,
445
+ 1747873779,
446
+ 1955562222,
447
+ 2024104815,
448
+ 2227730452,
449
+ 2361852424,
450
+ 2428436474,
451
+ 2756734187,
452
+ 3204031479,
453
+ 3329325298
454
+ ]);
455
+ function canonical(value2) {
456
+ if (Array.isArray(value2)) return `[${value2.map(canonical).join(",")}]`;
457
+ if (value2 && typeof value2 === "object") {
458
+ return `{${Object.keys(value2).sort().map((key2) => `${JSON.stringify(key2)}:${canonical(value2[key2])}`).join(",")}}`;
459
+ }
460
+ return JSON.stringify(value2);
461
+ }
462
+ function rotateRight(value2, count) {
463
+ return value2 >>> count | value2 << 32 - count;
464
+ }
465
+ function sha256(input) {
466
+ const bytes = new TextEncoder().encode(input);
467
+ const paddedLength = Math.ceil((bytes.length + 9) / 64) * 64;
468
+ const padded = new Uint8Array(paddedLength);
469
+ padded.set(bytes);
470
+ padded[bytes.length] = 128;
471
+ const view = new DataView(padded.buffer);
472
+ const bitLength = bytes.length * 8;
473
+ view.setUint32(paddedLength - 8, Math.floor(bitLength / 4294967296));
474
+ view.setUint32(paddedLength - 4, bitLength >>> 0);
475
+ const hash = new Uint32Array([
476
+ 1779033703,
477
+ 3144134277,
478
+ 1013904242,
479
+ 2773480762,
480
+ 1359893119,
481
+ 2600822924,
482
+ 528734635,
483
+ 1541459225
484
+ ]);
485
+ const words = new Uint32Array(64);
486
+ for (let offset = 0; offset < paddedLength; offset += 64) {
487
+ for (let index = 0; index < 16; index += 1) words[index] = view.getUint32(offset + index * 4);
488
+ for (let index = 16; index < 64; index += 1) {
489
+ const first = words[index - 15];
490
+ const second = words[index - 2];
491
+ const sigma0 = rotateRight(first, 7) ^ rotateRight(first, 18) ^ first >>> 3;
492
+ const sigma1 = rotateRight(second, 17) ^ rotateRight(second, 19) ^ second >>> 10;
493
+ words[index] = words[index - 16] + sigma0 + words[index - 7] + sigma1 >>> 0;
494
+ }
495
+ let a = hash[0];
496
+ let b = hash[1];
497
+ let c = hash[2];
498
+ let d = hash[3];
499
+ let e = hash[4];
500
+ let f = hash[5];
501
+ let g = hash[6];
502
+ let h = hash[7];
503
+ for (let index = 0; index < 64; index += 1) {
504
+ const sum1 = rotateRight(e, 6) ^ rotateRight(e, 11) ^ rotateRight(e, 25);
505
+ const choice = e & f ^ ~e & g;
506
+ const temporary1 = h + sum1 + choice + SHA256_CONSTANTS[index] + words[index] >>> 0;
507
+ const sum0 = rotateRight(a, 2) ^ rotateRight(a, 13) ^ rotateRight(a, 22);
508
+ const majority = a & b ^ a & c ^ b & c;
509
+ const temporary2 = sum0 + majority >>> 0;
510
+ h = g;
511
+ g = f;
512
+ f = e;
513
+ e = d + temporary1 >>> 0;
514
+ d = c;
515
+ c = b;
516
+ b = a;
517
+ a = temporary1 + temporary2 >>> 0;
518
+ }
519
+ hash[0] = hash[0] + a >>> 0;
520
+ hash[1] = hash[1] + b >>> 0;
521
+ hash[2] = hash[2] + c >>> 0;
522
+ hash[3] = hash[3] + d >>> 0;
523
+ hash[4] = hash[4] + e >>> 0;
524
+ hash[5] = hash[5] + f >>> 0;
525
+ hash[6] = hash[6] + g >>> 0;
526
+ hash[7] = hash[7] + h >>> 0;
527
+ }
528
+ return [...hash].map((word) => word.toString(16).padStart(8, "0")).join("");
529
+ }
530
+ function canonicalRetirementManifest(input) {
531
+ const manifest = retirementManifestSchema.parse(input);
532
+ const capabilities = {
533
+ ...manifest.capabilities,
534
+ participants: [...manifest.capabilities.participants].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0)
535
+ };
536
+ const resources = [...manifest.resources].sort((a, b) => {
537
+ const first = JSON.stringify([a.ownerId, a.resourceId]);
538
+ const second = JSON.stringify([b.ownerId, b.resourceId]);
539
+ return first < second ? -1 : first > second ? 1 : 0;
540
+ });
541
+ return canonical({ ...manifest, capabilities, resources });
542
+ }
543
+ function retirementManifestDigest(input) {
544
+ return sha256(canonicalRetirementManifest(input));
545
+ }
546
+
547
+ // src/client/purge-batch.ts
548
+ function emptyPurgeBatchResourceCounts() {
549
+ return { erase: 0, releaseReference: 0, retainShared: 0, retainCoordination: 0 };
550
+ }
551
+ function countPurgeBatchResources(items) {
552
+ const counts = emptyPurgeBatchResourceCounts();
553
+ for (const resource of items.flatMap((item) => item.plan?.manifest?.resources ?? [])) {
554
+ if (resource.disposition === "erase") counts.erase += 1;
555
+ else if (resource.disposition === "release-reference") counts.releaseReference += 1;
556
+ else if (resource.disposition === "retain-shared") counts.retainShared += 1;
557
+ else counts.retainCoordination += 1;
558
+ }
559
+ return counts;
560
+ }
561
+ function clonePurgeBatch(batch) {
562
+ return batch === null ? null : structuredClone(batch);
563
+ }
564
+
565
+ // src/client/model.ts
566
+ function value(result) {
567
+ if (!result.ok) throw new Error("Session Bin Remote request failed.", { cause: result.error });
568
+ return result.value;
569
+ }
570
+ function pendingPlans(raw) {
571
+ if (!Array.isArray(raw)) return [];
572
+ const valid = [];
573
+ for (const item of raw.slice(0, 64)) {
574
+ const parsed = archivePlanSchema.safeParse(item);
575
+ const plan = parsed.success ? parsed.data : planSchema.safeParse(item).data;
576
+ if (plan && !valid.some((row) => row.operationId === plan.operationId)) valid.push(plan);
577
+ }
578
+ return valid;
579
+ }
580
+ function purgePlans(raw) {
581
+ if (!Array.isArray(raw)) return [];
582
+ const valid = [];
583
+ for (const row of raw) {
584
+ const parsed = nativePurgePlanSchema.safeParse(row);
585
+ if (parsed.success && parsed.data.binding && parsed.data.manifest && parsed.data.expectedEntryId && !valid.some((plan) => plan.operationId === parsed.data.operationId)) valid.push(parsed.data);
586
+ }
587
+ return valid;
588
+ }
589
+ function purgeGrantSnapshots(raw) {
590
+ if (!Array.isArray(raw)) return [];
591
+ const valid = [];
592
+ const uuid = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
593
+ for (const row of raw) {
594
+ if (!row || typeof row !== "object") continue;
595
+ const { operationId, authorizationId } = row;
596
+ if (typeof operationId === "string" && operationId.length > 0 && operationId.length <= 1024 && typeof authorizationId === "string" && uuid.test(authorizationId) && !valid.some((item) => item.operationId === operationId)) valid.push({ operationId, authorizationId });
597
+ }
598
+ return valid;
599
+ }
600
+ function purgeGrantObservations(raw) {
601
+ if (!Array.isArray(raw)) return [];
602
+ const valid = [];
603
+ for (const row of raw) {
604
+ if (!row || typeof row !== "object") continue;
605
+ const { operationId } = row;
606
+ if (typeof operationId === "string" && operationId.length > 0 && operationId.length <= 1024 && !valid.some((item) => item.operationId === operationId)) valid.push({ operationId });
607
+ }
608
+ return valid;
609
+ }
610
+ function purgeBatchOperations(raw) {
611
+ if (!Array.isArray(raw)) return [];
612
+ return [...new Set(raw.filter((operationId) => typeof operationId === "string" && operationId.length > 0 && operationId.length <= 1024))];
613
+ }
614
+ function purgeCacheOverflow(...values) {
615
+ return values.some((value2) => Array.isArray(value2) && value2.length > 64);
616
+ }
617
+ function canonical2(value2) {
618
+ if (Array.isArray(value2)) return `[${value2.map(canonical2).join(",")}]`;
619
+ if (value2 && typeof value2 === "object") return `{${Object.keys(value2).sort().map((key2) => `${JSON.stringify(key2)}:${canonical2(value2[key2])}`).join(",")}}`;
620
+ return JSON.stringify(value2);
621
+ }
622
+ function capabilitiesSnapshot(value2) {
623
+ return { ...value2, participants: [...value2.participants].sort((a, b) => a.id < b.id ? -1 : a.id > b.id ? 1 : 0) };
624
+ }
625
+ function manifestKey(value2) {
626
+ return canonicalRetirementManifest(value2);
627
+ }
628
+ function frozenPurgeKey(plan) {
629
+ return canonical2([
630
+ plan.schemaVersion,
631
+ plan.action,
632
+ plan.operationId,
633
+ plan.sessionId,
634
+ plan.expectedEntryId,
635
+ plan.binding ? { ...plan.binding, capabilities: capabilitiesSnapshot(plan.binding.capabilities) } : null,
636
+ plan.manifest ? manifestKey(plan.manifest) : null
637
+ ]);
638
+ }
639
+ var SessionBinClientModel = class {
640
+ constructor(api, createStream, cache) {
641
+ this.api = api;
642
+ this.createStream = createStream;
643
+ this.cache = cache;
644
+ try {
645
+ this.state = { ...this.state, pending: pendingPlans(cache?.load()) };
646
+ } catch {
647
+ }
648
+ let purgePending = [];
649
+ let rawPurgePlans;
650
+ let rawPurgeGrants;
651
+ let rawPurgeObservations;
652
+ let rawPurgeBatchOperations;
653
+ try {
654
+ rawPurgePlans = cache?.loadPurge?.();
655
+ purgePending = purgePlans(rawPurgePlans);
656
+ this.state = { ...this.state, purgePending, busy: [...new Set(purgePending.map((plan) => plan.sessionId))] };
657
+ } catch {
658
+ }
659
+ const pendingIds = new Set(purgePending.map((plan) => plan.operationId));
660
+ try {
661
+ rawPurgeBatchOperations = cache?.loadPurgeBatchOperations?.();
662
+ for (const operationId of purgeBatchOperations(rawPurgeBatchOperations)) {
663
+ if (pendingIds.has(operationId)) this.batchPurgeOperationIds.add(operationId);
664
+ }
665
+ this.state = { ...this.state, purgeBatchOperationIds: [...this.batchPurgeOperationIds] };
666
+ } catch {
667
+ }
668
+ let grantsReadable = false;
669
+ let observationsReadable = false;
670
+ try {
671
+ if (cache && typeof cache.loadPurgeGrants !== "function") throw new Error("Grant cache unavailable.");
672
+ rawPurgeGrants = cache?.loadPurgeGrants?.();
673
+ for (const grant of purgeGrantSnapshots(rawPurgeGrants)) {
674
+ if (pendingIds.has(grant.operationId)) this.observedPurgeGrants.set(grant.operationId, grant.authorizationId);
675
+ }
676
+ grantsReadable = Boolean(cache);
677
+ } catch {
678
+ }
679
+ const observations = /* @__PURE__ */ new Set();
680
+ try {
681
+ if (cache && typeof cache.loadPurgeGrantObservations !== "function") throw new Error("Grant observation cache unavailable.");
682
+ rawPurgeObservations = cache?.loadPurgeGrantObservations?.();
683
+ for (const observation of purgeGrantObservations(rawPurgeObservations)) {
684
+ if (pendingIds.has(observation.operationId)) observations.add(observation.operationId);
685
+ }
686
+ observationsReadable = Boolean(cache);
687
+ } catch {
688
+ }
689
+ if (cache) {
690
+ const canPersist = typeof cache.stagePurgeGrantObservation === "function" && typeof cache.savePurgeGrants === "function";
691
+ for (const operationId of pendingIds) {
692
+ if (!this.observedPurgeGrants.has(operationId) && (observations.has(operationId) || !grantsReadable || !observationsReadable || !canPersist)) {
693
+ this.uncertainPurgeGrants.add(operationId);
694
+ }
695
+ }
696
+ }
697
+ const purgeCacheBlocked = Boolean(cache?.isPurgeCacheBlocked?.()) || purgeCacheOverflow(rawPurgePlans, rawPurgeGrants, rawPurgeObservations, rawPurgeBatchOperations);
698
+ this.state = { ...this.state, purgeCacheBlocked };
699
+ this.snapshot = structuredClone(this.state);
700
+ }
701
+ state = {
702
+ phase: "loading",
703
+ entries: [],
704
+ busy: [],
705
+ pending: [],
706
+ results: [],
707
+ error: null,
708
+ notice: null,
709
+ purgeConfirmation: null,
710
+ purgePending: [],
711
+ purgeResults: [],
712
+ purgeBatch: null,
713
+ purgeBatchOperationIds: [],
714
+ purgeCacheBlocked: false
715
+ };
716
+ snapshot = structuredClone(this.state);
717
+ listeners = /* @__PURE__ */ new Set();
718
+ lifetime = new AbortController();
719
+ stream;
720
+ consumption = Promise.resolve();
721
+ epoch = 0;
722
+ noticeSequence = 0;
723
+ checking = /* @__PURE__ */ new Set();
724
+ purgeExecuting = /* @__PURE__ */ new Set();
725
+ purgeChecking = /* @__PURE__ */ new Set();
726
+ purgeDiscovering = false;
727
+ settledPurgeIds = /* @__PURE__ */ new Set();
728
+ observedPurgeGrants = /* @__PURE__ */ new Map();
729
+ uncertainPurgeGrants = /* @__PURE__ */ new Set();
730
+ unpersistedPurgeGrants = /* @__PURE__ */ new Set();
731
+ batchPurgeOperationIds = /* @__PURE__ */ new Set();
732
+ confirmation = null;
733
+ purgeBatch = null;
734
+ purgeBatchRun = null;
735
+ purgePreparationEpoch = 0;
736
+ getSnapshot = () => this.snapshot;
737
+ subscribe = (listener) => {
738
+ this.listeners.add(listener);
739
+ return () => {
740
+ this.listeners.delete(listener);
741
+ };
742
+ };
743
+ publish(next) {
744
+ if (this.lifetime.signal.aborted) return;
745
+ this.state = { ...this.state, ...next };
746
+ this.snapshot = structuredClone(this.state);
747
+ for (const listener of this.listeners) listener();
748
+ }
749
+ persist(plans) {
750
+ try {
751
+ this.cache?.save(plans);
752
+ } catch {
753
+ }
754
+ }
755
+ pending(plans) {
756
+ this.persist(plans);
757
+ this.publish({ pending: plans });
758
+ }
759
+ async refresh() {
760
+ if (this.lifetime.signal.aborted) return;
761
+ const epoch = ++this.epoch;
762
+ this.publish({ phase: "loading", error: null });
763
+ await this.stream?.dispose();
764
+ if (epoch !== this.epoch || this.lifetime.signal.aborted) return;
765
+ const stream = this.createStream();
766
+ this.stream = stream;
767
+ this.consumption = (async () => {
768
+ try {
769
+ for await (const item of stream) {
770
+ if (epoch !== this.epoch || item.signal.aborted || this.lifetime.signal.aborted) continue;
771
+ const snapshot = snapshotSchema.parse(item.value);
772
+ this.publish({ entries: snapshot.entries, phase: "ready", error: null });
773
+ item.accept();
774
+ void this.checkPending(false);
775
+ void this.checkPurgePending();
776
+ void this.discoverPurgePending();
777
+ }
778
+ } catch {
779
+ if (epoch === this.epoch) this.publish({ phase: "error", error: "connection-failed" });
780
+ }
781
+ })();
782
+ }
783
+ async dispose() {
784
+ this.lifetime.abort();
785
+ ++this.epoch;
786
+ await this.stream?.dispose();
787
+ await this.consumption;
788
+ this.listeners.clear();
789
+ }
790
+ dismissNotice() {
791
+ this.publish({ notice: null });
792
+ }
793
+ hasActivePurgeBatch() {
794
+ return this.purgeBatch !== null && ["preparing", "confirming", "running", "paused"].includes(this.purgeBatch.phase);
795
+ }
796
+ isPurgeCacheBlocked() {
797
+ const blocked = this.state.purgeCacheBlocked || Boolean(this.cache?.isPurgeCacheBlocked?.());
798
+ if (blocked && !this.state.purgeCacheBlocked) this.publish({ purgeCacheBlocked: true });
799
+ return blocked;
800
+ }
801
+ rejectPurgeCacheCapacity() {
802
+ if (!this.isPurgeCacheBlocked()) return false;
803
+ this.publish({ error: "purge-cache-capacity", purgeCacheBlocked: true });
804
+ return true;
805
+ }
806
+ publishPurgeBatch() {
807
+ if (this.purgeBatch) this.purgeBatch.resourceCounts = countPurgeBatchResources(this.purgeBatch.items);
808
+ this.publish({ purgeBatch: clonePurgeBatch(this.purgeBatch) });
809
+ }
810
+ savePurgePending(plans, batchOperationId = null) {
811
+ const snapshots = plans.map((plan) => nativePurgePlanSchema.parse(plan));
812
+ const activeIds = new Set(snapshots.map((plan) => plan.operationId));
813
+ if (this.isPurgeCacheBlocked()) {
814
+ const removesGuard = this.state.purgePending.some((plan) => !activeIds.has(plan.operationId));
815
+ if (!removesGuard) this.publish({ purgePending: snapshots, purgeBatchOperationIds: [...this.batchPurgeOperationIds] });
816
+ return;
817
+ }
818
+ if (batchOperationId && activeIds.has(batchOperationId)) this.batchPurgeOperationIds.add(batchOperationId);
819
+ for (const operationId of this.batchPurgeOperationIds) {
820
+ if (!activeIds.has(operationId)) this.batchPurgeOperationIds.delete(operationId);
821
+ }
822
+ for (const operationId of this.observedPurgeGrants.keys()) {
823
+ if (!activeIds.has(operationId)) this.observedPurgeGrants.delete(operationId);
824
+ }
825
+ for (const operationId of this.uncertainPurgeGrants) {
826
+ if (!activeIds.has(operationId)) this.uncertainPurgeGrants.delete(operationId);
827
+ }
828
+ for (const operationId of this.unpersistedPurgeGrants) {
829
+ if (!activeIds.has(operationId)) this.unpersistedPurgeGrants.delete(operationId);
830
+ }
831
+ const batchOperationIds = [...this.batchPurgeOperationIds];
832
+ try {
833
+ this.cache?.savePurge?.(snapshots, batchOperationIds);
834
+ } catch {
835
+ if (this.cache?.isPurgeCacheBlocked?.()) this.publish({ purgeCacheBlocked: true });
836
+ }
837
+ if ([...this.observedPurgeGrants.keys()].some((operationId) => activeIds.has(operationId))) {
838
+ try {
839
+ this.persistPurgeGrants(activeIds);
840
+ } catch {
841
+ for (const operationId of this.observedPurgeGrants.keys()) {
842
+ if (activeIds.has(operationId)) this.unpersistedPurgeGrants.add(operationId);
843
+ }
844
+ }
845
+ }
846
+ this.publish({ purgePending: snapshots, purgeBatchOperationIds: batchOperationIds });
847
+ }
848
+ persistPurgeGrants(activeIds = new Set(this.state.purgePending.map((plan) => plan.operationId))) {
849
+ if (this.isPurgeCacheBlocked()) throw new Error("Deletion cache capacity exceeded.");
850
+ const grants = [...this.observedPurgeGrants].filter(([operationId]) => activeIds.has(operationId)).map(([operationId, authorizationId]) => ({ operationId, authorizationId }));
851
+ if (!this.cache) return;
852
+ if (typeof this.cache.savePurgeGrants !== "function") throw new Error("Grant cache unavailable.");
853
+ this.cache.savePurgeGrants(grants);
854
+ }
855
+ stagePurgeGrantObservation(operationId) {
856
+ if (this.isPurgeCacheBlocked()) throw new Error("Deletion cache capacity exceeded.");
857
+ if (!this.cache) return;
858
+ if (typeof this.cache.stagePurgeGrantObservation !== "function") {
859
+ this.uncertainPurgeGrants.add(operationId);
860
+ throw new Error("Grant observation cache unavailable.");
861
+ }
862
+ try {
863
+ this.cache.stagePurgeGrantObservation(operationId);
864
+ } catch {
865
+ this.uncertainPurgeGrants.add(operationId);
866
+ throw new Error("Grant observation could not be saved.");
867
+ }
868
+ }
869
+ validatePurgeOwnerState(plan, state) {
870
+ const manifestDigest = plan.manifest ? retirementManifestDigest(plan.manifest) : null;
871
+ if (!plan.binding || !plan.manifest || state.request.operationId !== plan.operationId || state.request.bin.entryVersion !== 2 || state.request.bin.entryId !== plan.expectedEntryId || !lifecycleEqual(state.request.expected, plan.binding.lifecycle) || manifestKey(state.manifest) !== manifestKey(plan.manifest) || state.request.manifestDigest !== manifestDigest) {
872
+ throw new Error("Deletion result changed its confirmed lifecycle or resource scope.");
873
+ }
874
+ const observed = this.observedPurgeGrants.get(plan.operationId);
875
+ if (observed !== void 0 && observed !== state.authorizationId) {
876
+ throw new Error("Deletion result changed its observed Host authorization grant.");
877
+ }
878
+ if (this.uncertainPurgeGrants.has(plan.operationId)) {
879
+ throw new Error("Deletion authorization continuity is uncertain.");
880
+ }
881
+ if (state.authorizationId === null) return;
882
+ if (observed === void 0) {
883
+ this.stagePurgeGrantObservation(plan.operationId);
884
+ this.observedPurgeGrants.set(plan.operationId, state.authorizationId);
885
+ this.unpersistedPurgeGrants.add(plan.operationId);
886
+ }
887
+ if (this.unpersistedPurgeGrants.has(plan.operationId)) {
888
+ try {
889
+ this.persistPurgeGrants();
890
+ this.unpersistedPurgeGrants.delete(plan.operationId);
891
+ } catch {
892
+ throw new Error("Deletion authorization grant could not be saved.");
893
+ }
894
+ }
895
+ }
896
+ purgeOutcome(plan, status, reason) {
897
+ const outcome = { operationId: plan.operationId, sessionId: plan.sessionId, entryId: plan.expectedEntryId, status, reason };
898
+ this.publish({ purgeResults: [outcome, ...this.state.purgeResults.filter((row) => row.operationId !== plan.operationId)].slice(0, 64) });
899
+ const item = this.purgeBatch?.items.find((row) => row.plan?.operationId === plan.operationId);
900
+ if (item) {
901
+ item.state = "settled";
902
+ item.outcome = structuredClone(outcome);
903
+ item.reason = reason;
904
+ this.publishPurgeBatch();
905
+ }
906
+ return outcome;
907
+ }
908
+ publishConfirmation() {
909
+ const confirmation = this.confirmation;
910
+ this.publish({ purgeConfirmation: confirmation ? {
911
+ plan: nativePurgePlanSchema.parse(confirmation.plan),
912
+ title: confirmation.title,
913
+ acknowledged: confirmation.acknowledged
914
+ } : null });
915
+ }
916
+ async requestPurgePlan(target) {
917
+ if (this.rejectPurgeCacheCapacity()) return { plan: null, reason: "purge-cache-capacity" };
918
+ try {
919
+ const plan = nativePurgePlanSchema.parse(value(await this.api.preparePurge({
920
+ sessionId: target.sessionId,
921
+ operationId: crypto.randomUUID()
922
+ }, this.lifetime.signal)));
923
+ this.lifetime.signal.throwIfAborted();
924
+ if (plan.sessionId !== target.sessionId || plan.expectedEntryId !== target.entryId || !this.state.entries.some((row) => row.sessionId === target.sessionId && row.entryId === target.entryId)) {
925
+ return { plan: null, reason: "entry-changed" };
926
+ }
927
+ return { plan: nativePurgePlanSchema.parse(plan), reason: null };
928
+ } catch {
929
+ return { plan: null, reason: "connection-failed" };
930
+ }
931
+ }
932
+ async preparePurge(entry, title) {
933
+ const target = { sessionId: entry.sessionId, entryId: entry.entryId, title };
934
+ if (this.rejectPurgeCacheCapacity() || this.state.phase !== "ready" || this.state.busy.includes(target.sessionId) || this.confirmation || this.hasActivePurgeBatch() || this.state.purgePending.length > 0 || this.state.pending.some((plan) => plan.schemaVersion === 2 && plan.sessionId === target.sessionId) || this.lifetime.signal.aborted) return null;
935
+ this.publish({ busy: [...this.state.busy, target.sessionId], error: null });
936
+ try {
937
+ const prepared = await this.requestPurgePlan(target);
938
+ if (!prepared.plan) {
939
+ this.publish({ error: prepared.reason });
940
+ return null;
941
+ }
942
+ this.confirmation = {
943
+ plan: prepared.plan,
944
+ title: target.title,
945
+ acknowledged: false,
946
+ batchId: null,
947
+ batchItemIndex: null,
948
+ replacesOperationId: null
949
+ };
950
+ this.publishConfirmation();
951
+ return nativePurgePlanSchema.parse(prepared.plan);
952
+ } finally {
953
+ this.publish({ busy: this.state.busy.filter((id) => id !== target.sessionId) });
954
+ }
955
+ }
956
+ acknowledgePurge(acknowledged) {
957
+ if (!this.confirmation) return;
958
+ this.confirmation = { ...this.confirmation, acknowledged };
959
+ this.publishConfirmation();
960
+ }
961
+ cancelPurge() {
962
+ this.confirmation = null;
963
+ this.publishConfirmation();
964
+ }
965
+ async confirmPurge() {
966
+ const confirmation = this.confirmation;
967
+ if (!confirmation?.acknowledged || confirmation.plan.blockers.length || !confirmation.plan.binding || !confirmation.plan.manifest || this.state.phase !== "ready" || this.lifetime.signal.aborted || this.rejectPurgeCacheCapacity()) return;
968
+ const connectionEpoch = this.epoch;
969
+ const plan = nativePurgePlanSchema.parse(confirmation.plan);
970
+ const replacedOperationId = confirmation.replacesOperationId;
971
+ if (replacedOperationId === null) {
972
+ if (this.state.purgePending.length > 0 || this.hasActivePurgeBatch()) return;
973
+ this.cancelPurge();
974
+ return this.runPurge(plan, false, connectionEpoch);
975
+ }
976
+ const previous = this.state.purgePending.find((row) => row.operationId === replacedOperationId);
977
+ const batch = confirmation.batchId === null ? null : this.purgeBatch;
978
+ const item = batch && confirmation.batchItemIndex !== null ? batch.items[confirmation.batchItemIndex] : null;
979
+ const associationIsCurrent = () => {
980
+ const current = this.state.purgePending.find((row) => row.operationId === replacedOperationId);
981
+ if (this.confirmation !== confirmation || !current || frozenPurgeKey(current) !== frozenPurgeKey(previous) || this.state.purgePending.some((row) => row.operationId !== replacedOperationId) || this.observedPurgeGrants.has(replacedOperationId) || this.uncertainPurgeGrants.has(replacedOperationId) || plan.sessionId !== current.sessionId || plan.expectedEntryId !== current.expectedEntryId) return false;
982
+ if (confirmation.batchId === null) return !this.hasActivePurgeBatch();
983
+ return batch !== null && batch === this.purgeBatch && batch.batchId === confirmation.batchId && (batch.phase === "paused" && !batch.stopRequested || batch.phase === "cancelled" && batch.stopRequested) && item != null && batch.items[confirmation.batchItemIndex] === item && item.state === "settled" && item.plan?.operationId === replacedOperationId && item.outcome?.operationId === replacedOperationId && item.outcome.reason === "deletion-result-missing" && item.target.sessionId === plan.sessionId && item.target.entryId === plan.expectedEntryId;
984
+ };
985
+ if (!previous || this.purgeExecuting.has(replacedOperationId) || this.purgeChecking.has(replacedOperationId) || !associationIsCurrent()) return;
986
+ this.purgeChecking.add(replacedOperationId);
987
+ try {
988
+ if (await this.readPurgeOperation(previous) !== null) {
989
+ if (associationIsCurrent()) this.publish({ error: "deletion-pending" });
990
+ return;
991
+ }
992
+ } catch {
993
+ if (associationIsCurrent()) this.publish({ error: "deletion-pending" });
994
+ return;
995
+ } finally {
996
+ this.purgeChecking.delete(replacedOperationId);
997
+ }
998
+ if (this.state.phase !== "ready" || this.epoch !== connectionEpoch || this.lifetime.signal.aborted || !associationIsCurrent()) return;
999
+ const batchOrigin = this.batchPurgeOperationIds.has(replacedOperationId) || confirmation.batchId !== null;
1000
+ this.savePurgePending([
1001
+ ...this.state.purgePending.filter((row) => row.operationId !== replacedOperationId),
1002
+ plan
1003
+ ], batchOrigin ? plan.operationId : null);
1004
+ this.publish({ purgeResults: this.state.purgeResults.filter((row) => row.operationId !== replacedOperationId) });
1005
+ if (item) {
1006
+ item.plan = plan;
1007
+ item.state = "running";
1008
+ item.outcome = null;
1009
+ item.reason = null;
1010
+ this.publishPurgeBatch();
1011
+ }
1012
+ this.cancelPurge();
1013
+ return this.runPurge(plan, batchOrigin, connectionEpoch);
1014
+ }
1015
+ async preparePurgeBatch(scope, titlesBySessionId = {}) {
1016
+ if (this.rejectPurgeCacheCapacity() || this.state.phase !== "ready" || this.confirmation || this.hasActivePurgeBatch() || this.state.purgePending.length > 0 || this.lifetime.signal.aborted) return null;
1017
+ const selected = scope.kind === "selection" ? new Set(scope.entryIds) : null;
1018
+ const seen = /* @__PURE__ */ new Set();
1019
+ const targets = this.state.entries.filter((row) => selected === null || selected.has(row.entryId)).filter((row) => {
1020
+ const key2 = `${row.sessionId}\0${row.entryId}`;
1021
+ if (seen.has(key2)) return false;
1022
+ seen.add(key2);
1023
+ return true;
1024
+ }).map((row) => {
1025
+ const title = titlesBySessionId[row.sessionId];
1026
+ return { sessionId: row.sessionId, entryId: row.entryId, title: typeof title === "string" ? title : row.sessionId };
1027
+ });
1028
+ if (!targets.length) return null;
1029
+ ++this.purgePreparationEpoch;
1030
+ const batch = {
1031
+ batchId: crypto.randomUUID(),
1032
+ scope: scope.kind,
1033
+ phase: "preparing",
1034
+ frozenCount: targets.length,
1035
+ acknowledged: false,
1036
+ stopRequested: false,
1037
+ items: targets.map((target) => ({ target, state: "preparing", plan: null, reason: null, outcome: null })),
1038
+ resourceCounts: emptyPurgeBatchResourceCounts()
1039
+ };
1040
+ this.purgeBatch = batch;
1041
+ this.publish({ error: null });
1042
+ this.publishPurgeBatch();
1043
+ for (let index = 0; index < batch.items.length; index += 1) {
1044
+ if (batch.stopRequested || this.lifetime.signal.aborted) break;
1045
+ const item = batch.items[index];
1046
+ if (!item) continue;
1047
+ if (this.state.pending.some((plan) => plan.schemaVersion === 2 && plan.sessionId === item.target.sessionId)) {
1048
+ item.state = "blocked";
1049
+ item.reason = "pending-result";
1050
+ this.publishPurgeBatch();
1051
+ continue;
1052
+ }
1053
+ this.publish({ busy: [.../* @__PURE__ */ new Set([...this.state.busy, item.target.sessionId])] });
1054
+ const prepared = await this.requestPurgePlan(item.target);
1055
+ this.publish({ busy: this.state.busy.filter((id) => id !== item.target.sessionId) });
1056
+ if (batch !== this.purgeBatch || batch.stopRequested || this.lifetime.signal.aborted) {
1057
+ item.state = "cancelled";
1058
+ item.reason = null;
1059
+ this.publishPurgeBatch();
1060
+ break;
1061
+ }
1062
+ if (!prepared.plan) {
1063
+ item.state = "blocked";
1064
+ item.reason = prepared.reason;
1065
+ this.publishPurgeBatch();
1066
+ continue;
1067
+ }
1068
+ item.plan = prepared.plan;
1069
+ if (prepared.plan.blockers.length || !prepared.plan.binding || !prepared.plan.manifest) {
1070
+ item.state = "blocked";
1071
+ item.reason = prepared.plan.blockers[0]?.code ?? "deletion-not-ready";
1072
+ } else item.state = "ready";
1073
+ this.publishPurgeBatch();
1074
+ }
1075
+ if (batch !== this.purgeBatch) return null;
1076
+ if (batch.stopRequested || this.lifetime.signal.aborted) {
1077
+ for (const item of batch.items) {
1078
+ if (item.state === "preparing" || item.state === "ready") item.state = "cancelled";
1079
+ }
1080
+ batch.phase = "cancelled";
1081
+ } else batch.phase = "confirming";
1082
+ this.publishPurgeBatch();
1083
+ return clonePurgeBatch(batch);
1084
+ }
1085
+ acknowledgePurgeBatch(acknowledged) {
1086
+ if (!this.purgeBatch || this.purgeBatch.phase !== "confirming") return;
1087
+ this.purgeBatch.acknowledged = acknowledged;
1088
+ this.publishPurgeBatch();
1089
+ }
1090
+ async runPurgeBatch() {
1091
+ if (this.purgeBatchRun) return this.purgeBatchRun;
1092
+ const batch = this.purgeBatch;
1093
+ if (!batch || this.state.phase !== "ready" || this.rejectPurgeCacheCapacity() || this.lifetime.signal.aborted || this.confirmation) return;
1094
+ if (batch.phase === "confirming" && !batch.acknowledged) return;
1095
+ if (batch.phase !== "confirming" && batch.phase !== "paused") return;
1096
+ const terminal = (outcome) => outcome !== null && ["success", "rejected", "conflict"].includes(outcome.status);
1097
+ if (batch.phase === "paused" && batch.items.some((item) => item.state === "settled" && !terminal(item.outcome))) return;
1098
+ if (this.state.purgePending.length > 0) {
1099
+ batch.phase = "paused";
1100
+ this.publishPurgeBatch();
1101
+ return clonePurgeBatch(batch) ?? void 0;
1102
+ }
1103
+ const connectionEpoch = this.epoch;
1104
+ batch.phase = "running";
1105
+ this.publishPurgeBatch();
1106
+ this.purgeBatchRun = (async () => {
1107
+ for (const item of batch.items) {
1108
+ if (batch !== this.purgeBatch || this.lifetime.signal.aborted) break;
1109
+ if (batch.stopRequested) break;
1110
+ if (item.state !== "ready") continue;
1111
+ if (this.state.phase !== "ready" || this.epoch !== connectionEpoch || this.state.purgePending.length > 0) {
1112
+ batch.phase = "paused";
1113
+ this.publishPurgeBatch();
1114
+ return clonePurgeBatch(batch) ?? void 0;
1115
+ }
1116
+ const plan = item.plan;
1117
+ if (!plan) {
1118
+ item.state = "blocked";
1119
+ item.reason = "deletion-not-ready";
1120
+ this.publishPurgeBatch();
1121
+ continue;
1122
+ }
1123
+ item.state = "running";
1124
+ item.reason = null;
1125
+ this.publishPurgeBatch();
1126
+ if (batch.stopRequested || this.lifetime.signal.aborted) {
1127
+ item.state = "cancelled";
1128
+ break;
1129
+ }
1130
+ if (this.state.phase !== "ready" || this.epoch !== connectionEpoch) {
1131
+ item.state = "ready";
1132
+ batch.phase = "paused";
1133
+ this.publishPurgeBatch();
1134
+ return clonePurgeBatch(batch) ?? void 0;
1135
+ }
1136
+ const outcome = await this.runPurge(plan, true, connectionEpoch);
1137
+ if (!terminal(outcome)) {
1138
+ batch.phase = batch.stopRequested ? "cancelled" : "paused";
1139
+ if (batch.stopRequested) {
1140
+ for (const unsent of batch.items) {
1141
+ if (unsent.state === "preparing" || unsent.state === "ready") unsent.state = "cancelled";
1142
+ }
1143
+ }
1144
+ this.publishPurgeBatch();
1145
+ return clonePurgeBatch(batch) ?? void 0;
1146
+ }
1147
+ }
1148
+ if (batch.stopRequested || this.lifetime.signal.aborted) {
1149
+ for (const item of batch.items) {
1150
+ if (item.state === "preparing" || item.state === "ready") item.state = "cancelled";
1151
+ }
1152
+ batch.phase = "cancelled";
1153
+ } else batch.phase = "done";
1154
+ this.publishPurgeBatch();
1155
+ return clonePurgeBatch(batch) ?? void 0;
1156
+ })();
1157
+ try {
1158
+ return await this.purgeBatchRun;
1159
+ } finally {
1160
+ this.purgeBatchRun = null;
1161
+ }
1162
+ }
1163
+ stopPurgeBatch() {
1164
+ if (!this.purgeBatch || !this.hasActivePurgeBatch()) return;
1165
+ const batch = this.purgeBatch;
1166
+ ++this.purgePreparationEpoch;
1167
+ if (this.confirmation?.batchId === batch.batchId) {
1168
+ this.confirmation = null;
1169
+ this.publishConfirmation();
1170
+ }
1171
+ batch.stopRequested = true;
1172
+ for (const item of batch.items) {
1173
+ if (item.state === "preparing" || item.state === "ready") item.state = "cancelled";
1174
+ }
1175
+ if (batch.phase !== "preparing" && (batch.phase !== "running" || !batch.items.some((item) => item.state === "running"))) {
1176
+ batch.phase = "cancelled";
1177
+ }
1178
+ this.publishPurgeBatch();
1179
+ }
1180
+ dismissPurgeBatch() {
1181
+ if (!this.purgeBatch || !["done", "cancelled"].includes(this.purgeBatch.phase)) return;
1182
+ ++this.purgePreparationEpoch;
1183
+ if (this.confirmation?.batchId === this.purgeBatch.batchId) this.cancelPurge();
1184
+ this.purgeBatch = null;
1185
+ this.publishPurgeBatch();
1186
+ }
1187
+ async readPurgeOperation(plan) {
1188
+ const raw = value(await this.api.getPurgeOperation(plan.operationId, this.lifetime.signal));
1189
+ if (raw === null) {
1190
+ if (this.observedPurgeGrants.has(plan.operationId) || this.uncertainPurgeGrants.has(plan.operationId)) {
1191
+ throw new Error("An admitted deletion lost its saved Host journal.");
1192
+ }
1193
+ return null;
1194
+ }
1195
+ const operation = purgeOperationSchema.parse(raw);
1196
+ if (operation.schemaVersion !== 2 || frozenPurgeKey(operation.plan) !== frozenPurgeKey(plan)) throw new Error("Deletion receipt changed its confirmed binding.");
1197
+ if (this.uncertainPurgeGrants.has(plan.operationId)) throw new Error("Deletion authorization continuity is uncertain.");
1198
+ const observed = this.observedPurgeGrants.get(plan.operationId);
1199
+ if (observed !== void 0 && operation.authorizationId !== observed) throw new Error("Deletion journal lost its observed authorization grant.");
1200
+ if (operation.ownerState) this.validatePurgeOwnerState(plan, operation.ownerState);
1201
+ return operation;
1202
+ }
1203
+ async discoverPurgePending() {
1204
+ if (this.purgeDiscovering || this.lifetime.signal.aborted || typeof this.api.purgeOperations !== "function") return;
1205
+ this.purgeDiscovering = true;
1206
+ try {
1207
+ const operations = purgeOperationsSchema.parse(value(await this.api.purgeOperations(this.lifetime.signal)));
1208
+ this.lifetime.signal.throwIfAborted();
1209
+ for (const operation of operations.filter((row) => row.schemaVersion === 2 && row.phase !== "done")) {
1210
+ const plan = nativePurgePlanSchema.parse(operation.plan);
1211
+ if (this.settledPurgeIds.has(plan.operationId) || this.purgeExecuting.has(plan.operationId) || this.purgeChecking.has(plan.operationId)) continue;
1212
+ const existing = this.state.purgePending.find((row) => row.operationId === plan.operationId);
1213
+ if (existing && frozenPurgeKey(existing) !== frozenPurgeKey(plan)) throw new Error("Discovered deletion changed its confirmed binding.");
1214
+ if (!existing) this.savePurgePending([...this.state.purgePending, plan]);
1215
+ this.publish({ busy: [.../* @__PURE__ */ new Set([...this.state.busy, plan.sessionId])] });
1216
+ if (operation.result) this.settlePurge(plan, operation.result, false);
1217
+ else {
1218
+ this.purgeOutcome(plan, "pending", "deletion-pending");
1219
+ this.publish({ error: "deletion-pending" });
1220
+ }
1221
+ }
1222
+ } catch {
1223
+ this.publish({ error: "deletion-pending" });
1224
+ } finally {
1225
+ this.purgeDiscovering = false;
1226
+ }
1227
+ }
1228
+ /** Automatic checks and reload only observe; no missing or unfinished deletion is replayed. */
1229
+ async checkPurgePending() {
1230
+ for (const plan of [...this.state.purgePending]) {
1231
+ if (this.purgeExecuting.has(plan.operationId) || this.purgeChecking.has(plan.operationId) || this.lifetime.signal.aborted) continue;
1232
+ this.purgeChecking.add(plan.operationId);
1233
+ try {
1234
+ const operation = await this.readPurgeOperation(plan);
1235
+ if (operation?.result) this.settlePurge(plan, operation.result, false);
1236
+ else {
1237
+ const reason = operation === null ? "deletion-result-missing" : "deletion-pending";
1238
+ this.purgeOutcome(plan, "pending", reason);
1239
+ this.publish({ error: reason });
1240
+ }
1241
+ } catch {
1242
+ this.publish({ error: this.isPurgeCacheBlocked() ? "purge-cache-capacity" : "deletion-pending" });
1243
+ } finally {
1244
+ this.purgeChecking.delete(plan.operationId);
1245
+ }
1246
+ }
1247
+ }
1248
+ /** Explicit continuation may only submit a known saved operation's original plan. */
1249
+ async retryPurge(operationId) {
1250
+ const plan = this.state.purgePending.find((row) => row.operationId === operationId);
1251
+ if (this.rejectPurgeCacheCapacity() || this.state.phase !== "ready" || !plan || this.purgeExecuting.has(operationId) || this.purgeChecking.has(operationId) || this.lifetime.signal.aborted) return;
1252
+ const connectionEpoch = this.epoch;
1253
+ this.purgeChecking.add(operationId);
1254
+ try {
1255
+ const operation = await this.readPurgeOperation(plan);
1256
+ this.lifetime.signal.throwIfAborted();
1257
+ if (operation === null) {
1258
+ this.publish({ error: "deletion-result-missing" });
1259
+ return this.purgeOutcome(plan, "pending", "deletion-result-missing");
1260
+ }
1261
+ if (operation.phase === "done" && operation.result) return this.settlePurge(plan, operation.result);
1262
+ if (this.state.phase !== "ready" || this.epoch !== connectionEpoch) {
1263
+ return this.purgeOutcome(plan, "pending", "connection-failed");
1264
+ }
1265
+ return await this.runPurge(plan, false, connectionEpoch);
1266
+ } catch {
1267
+ this.publish({ error: "deletion-pending" });
1268
+ return this.purgeOutcome(plan, "pending", "deletion-pending");
1269
+ } finally {
1270
+ this.purgeChecking.delete(operationId);
1271
+ }
1272
+ }
1273
+ /** A confirmed missing operation requires fresh preparation and another explicit confirmation. */
1274
+ async preparePurgeAgain(operationId, title) {
1275
+ const previous = this.state.purgePending.find((plan) => plan.operationId === operationId);
1276
+ const belongsToBatch = this.batchPurgeOperationIds.has(operationId);
1277
+ const batch = this.purgeBatch;
1278
+ const batchItemIndex = batch?.items.findIndex((item2) => item2.plan?.operationId === operationId) ?? -1;
1279
+ const item = batch && batchItemIndex >= 0 ? batch.items[batchItemIndex] : null;
1280
+ const batchReplacement = belongsToBatch && batch !== null && (batch.phase === "paused" && !batch.stopRequested || batch.phase === "cancelled" && batch.stopRequested) && item?.state === "settled" && item.outcome?.operationId === operationId && item.outcome.reason === "deletion-result-missing";
1281
+ if (this.rejectPurgeCacheCapacity() || this.state.phase !== "ready" || !previous || this.confirmation || this.purgeExecuting.has(operationId) || this.purgeChecking.has(operationId) || this.lifetime.signal.aborted || this.hasActivePurgeBatch() && !batchReplacement || this.state.purgePending.some((plan) => plan.operationId !== operationId)) return null;
1282
+ const connectionEpoch = this.epoch;
1283
+ const preparationEpoch = ++this.purgePreparationEpoch;
1284
+ const associationIsCurrent = () => {
1285
+ const current = this.state.purgePending.find((plan) => plan.operationId === operationId);
1286
+ if (this.state.phase !== "ready" || this.epoch !== connectionEpoch || preparationEpoch !== this.purgePreparationEpoch || this.confirmation !== null || !current || frozenPurgeKey(current) !== frozenPurgeKey(previous) || this.state.purgePending.some((plan) => plan.operationId !== operationId)) return false;
1287
+ if (!batchReplacement) return !this.hasActivePurgeBatch();
1288
+ return batch !== null && batch === this.purgeBatch && (batch.phase === "paused" && !batch.stopRequested || batch.phase === "cancelled" && batch.stopRequested) && batch.items[batchItemIndex] === item && item?.state === "settled" && item.plan?.operationId === operationId && item.outcome?.operationId === operationId && item.outcome.reason === "deletion-result-missing";
1289
+ };
1290
+ this.purgeChecking.add(operationId);
1291
+ try {
1292
+ const operation = await this.readPurgeOperation(previous);
1293
+ if (!associationIsCurrent()) return null;
1294
+ if (operation !== null) {
1295
+ this.publish({ error: "deletion-pending" });
1296
+ return null;
1297
+ }
1298
+ this.lifetime.signal.throwIfAborted();
1299
+ const entry = this.state.entries.find((row) => row.sessionId === previous.sessionId && row.entryId === previous.expectedEntryId);
1300
+ if (!entry) {
1301
+ this.publish({ error: "entry-changed" });
1302
+ return null;
1303
+ }
1304
+ const prepared = await this.requestPurgePlan(entry);
1305
+ if (!associationIsCurrent()) return null;
1306
+ if (!prepared.plan) {
1307
+ this.publish({ error: prepared.reason });
1308
+ return null;
1309
+ }
1310
+ this.publish({ error: null });
1311
+ if (!associationIsCurrent()) return null;
1312
+ this.confirmation = {
1313
+ plan: prepared.plan,
1314
+ title,
1315
+ acknowledged: false,
1316
+ batchId: batchReplacement ? batch.batchId : null,
1317
+ batchItemIndex: batchReplacement ? batchItemIndex : null,
1318
+ replacesOperationId: operationId
1319
+ };
1320
+ this.publishConfirmation();
1321
+ return nativePurgePlanSchema.parse(prepared.plan);
1322
+ } catch {
1323
+ if (associationIsCurrent()) this.publish({ error: "deletion-pending" });
1324
+ return null;
1325
+ } finally {
1326
+ this.purgeChecking.delete(operationId);
1327
+ }
1328
+ }
1329
+ async discardMissingPurge(operationId) {
1330
+ const plan = this.state.purgePending.find((row) => row.operationId === operationId);
1331
+ const belongsToBatch = this.batchPurgeOperationIds.has(operationId) || (this.purgeBatch?.items.some((item) => item.plan?.operationId === operationId) ?? false);
1332
+ if (this.rejectPurgeCacheCapacity() || belongsToBatch || !plan || this.purgeExecuting.has(operationId) || this.purgeChecking.has(operationId) || this.lifetime.signal.aborted) return false;
1333
+ this.purgeChecking.add(operationId);
1334
+ try {
1335
+ if (await this.readPurgeOperation(plan) !== null) return false;
1336
+ this.lifetime.signal.throwIfAborted();
1337
+ this.savePurgePending(this.state.purgePending.filter((row) => row.operationId !== operationId));
1338
+ if (!this.state.purgePending.some((row) => row.sessionId === plan.sessionId)) this.publish({ busy: this.state.busy.filter((id) => id !== plan.sessionId) });
1339
+ this.publish({ error: this.state.purgePending.length ? "deletion-pending" : null });
1340
+ return true;
1341
+ } catch {
1342
+ this.publish({ error: "deletion-pending" });
1343
+ return false;
1344
+ } finally {
1345
+ this.purgeChecking.delete(operationId);
1346
+ }
1347
+ }
1348
+ settlePurge(plan, raw, announce = true) {
1349
+ const result = purgeResultSchema.parse(raw);
1350
+ const admitted = ["success", "pending-recovery", "partial-failure"].includes(result.status);
1351
+ if (result.operationId !== plan.operationId || result.sessionId !== plan.sessionId || result.entryId !== null && result.entryId !== plan.expectedEntryId || admitted && result.entryId !== plan.expectedEntryId) {
1352
+ throw new Error("Deletion result changed its confirmed lifecycle or resource scope.");
1353
+ }
1354
+ if (this.uncertainPurgeGrants.has(plan.operationId)) throw new Error("Deletion authorization continuity is uncertain.");
1355
+ if (this.observedPurgeGrants.has(plan.operationId) && (result.status === "rejected" || result.status === "conflict" || !result.ownerState)) {
1356
+ throw new Error("An admitted deletion cannot lose its observed owner authorization.");
1357
+ }
1358
+ if (result.ownerState) this.validatePurgeOwnerState(plan, result.ownerState);
1359
+ const terminal = ["success", "rejected", "conflict"].includes(result.status);
1360
+ const cacheBlocked = this.isPurgeCacheBlocked();
1361
+ if (terminal && !cacheBlocked) {
1362
+ this.settledPurgeIds.add(plan.operationId);
1363
+ this.savePurgePending(this.state.purgePending.filter((row) => row.operationId !== plan.operationId));
1364
+ this.publish({ busy: this.state.busy.filter((id) => id !== plan.sessionId) });
1365
+ }
1366
+ const outcome = this.purgeOutcome(plan, result.status, result.reason);
1367
+ this.publish({ error: cacheBlocked ? "purge-cache-capacity" : terminal ? this.state.purgePending.length ? "deletion-pending" : null : "deletion-pending" });
1368
+ if (!cacheBlocked && (announce || terminal)) this.publish({ notice: {
1369
+ sequence: ++this.noticeSequence,
1370
+ kind: result.status === "success" ? "deleted" : "failed",
1371
+ sessionId: result.sessionId,
1372
+ entryId: result.entryId,
1373
+ reason: terminal ? result.reason : "deletion-pending"
1374
+ } });
1375
+ return outcome;
1376
+ }
1377
+ async runPurge(input, batchOrigin = false, connectionEpoch = this.epoch) {
1378
+ const plan = nativePurgePlanSchema.parse(input);
1379
+ const alreadyPending = this.state.purgePending.some((row) => row.operationId === plan.operationId);
1380
+ const cacheBlocked = this.rejectPurgeCacheCapacity();
1381
+ if (cacheBlocked || this.state.purgePending.length > 0 && !alreadyPending || this.purgeExecuting.has(plan.operationId) || this.lifetime.signal.aborted) {
1382
+ return this.purgeOutcome(plan, "pending", cacheBlocked ? "purge-cache-capacity" : "deletion-pending");
1383
+ }
1384
+ this.purgeExecuting.add(plan.operationId);
1385
+ this.publish({ busy: [.../* @__PURE__ */ new Set([...this.state.busy, plan.sessionId])], error: null });
1386
+ this.savePurgePending(
1387
+ [...this.state.purgePending.filter((row) => row.operationId !== plan.operationId), plan],
1388
+ batchOrigin ? plan.operationId : null
1389
+ );
1390
+ try {
1391
+ this.lifetime.signal.throwIfAborted();
1392
+ if (this.state.phase !== "ready" || this.epoch !== connectionEpoch) {
1393
+ this.publish({ error: "connection-failed" });
1394
+ return this.purgeOutcome(plan, "pending", "connection-failed");
1395
+ }
1396
+ return this.settlePurge(plan, value(await this.api.executePurge(nativePurgePlanSchema.parse(plan), this.lifetime.signal)));
1397
+ } catch {
1398
+ try {
1399
+ const operation = await this.readPurgeOperation(plan);
1400
+ if (operation?.result) return this.settlePurge(plan, operation.result);
1401
+ if (operation === null) {
1402
+ this.publish({ error: "deletion-result-missing" });
1403
+ return this.purgeOutcome(plan, "pending", "deletion-result-missing");
1404
+ }
1405
+ } catch {
1406
+ }
1407
+ this.publish({ error: "deletion-pending" });
1408
+ return this.purgeOutcome(plan, "pending", "deletion-pending");
1409
+ } finally {
1410
+ this.purgeExecuting.delete(plan.operationId);
1411
+ if (!this.state.purgePending.some((row) => row.sessionId === plan.sessionId)) this.publish({ busy: this.state.busy.filter((id) => id !== plan.sessionId) });
1412
+ }
1413
+ }
1414
+ async unarchive(entry) {
1415
+ return this.run(entry.sessionId, entry.entryId);
1416
+ }
1417
+ async unarchiveMany(entries) {
1418
+ const targets = entries.map((entry) => ({ sessionId: entry.sessionId, entryId: entry.entryId }));
1419
+ const results = [];
1420
+ for (const target of targets) {
1421
+ if (this.lifetime.signal.aborted) break;
1422
+ results.push(await this.unarchive(target));
1423
+ }
1424
+ this.publish({ results });
1425
+ return results;
1426
+ }
1427
+ /** Baselines only query receipts. Explicit retry can resend the same missing
1428
+ * v2 unarchive identity/plan; v1 bin/restore plans remain query-only forever.
1429
+ */
1430
+ async checkPending(retryMissing = true) {
1431
+ for (const plan of [...this.state.pending]) {
1432
+ if (this.checking.has(plan.operationId) || this.state.busy.includes(plan.sessionId) || this.lifetime.signal.aborted) continue;
1433
+ this.checking.add(plan.operationId);
1434
+ try {
1435
+ const operation = value(await this.api.getOperation(plan.operationId, this.lifetime.signal));
1436
+ if (operation?.result) {
1437
+ if (operation.plan.operationId !== plan.operationId || operation.plan.sessionId !== plan.sessionId || operation.plan.action !== plan.action || operation.schemaVersion !== plan.schemaVersion) throw new Error("Receipt identity changed.");
1438
+ if (plan.schemaVersion === 1) {
1439
+ this.pending(this.state.pending.filter((row) => row.operationId !== plan.operationId));
1440
+ this.publish({ error: this.state.pending.length ? "pending-result" : null });
1441
+ } else if (operation.result.action === "unarchive") this.settle(plan, operation.result);
1442
+ } else if (operation === null && retryMissing && plan.schemaVersion === 2) {
1443
+ this.settle(plan, value(await this.api.execute(plan, this.lifetime.signal)));
1444
+ } else if (plan.schemaVersion === 1) this.publish({ error: "legacy-pending" });
1445
+ } catch {
1446
+ this.publish({ error: "pending-result" });
1447
+ } finally {
1448
+ this.checking.delete(plan.operationId);
1449
+ }
1450
+ }
1451
+ }
1452
+ settle(plan, result) {
1453
+ if (result.operationId !== plan.operationId || result.sessionId !== plan.sessionId || result.action !== plan.action || result.status === "success" && result.entryId !== plan.expected.entryId) {
1454
+ throw new Error("Receipt identity changed.");
1455
+ }
1456
+ const outcome = {
1457
+ sessionId: result.sessionId,
1458
+ entryId: result.entryId,
1459
+ status: result.status,
1460
+ reason: result.reason
1461
+ };
1462
+ this.pending(this.state.pending.filter((row) => row.operationId !== plan.operationId));
1463
+ this.publish({
1464
+ results: [outcome],
1465
+ error: this.state.pending.length ? "pending-result" : null,
1466
+ notice: {
1467
+ sequence: ++this.noticeSequence,
1468
+ kind: result.status === "success" ? "unarchived" : "failed",
1469
+ sessionId: result.sessionId,
1470
+ entryId: result.entryId,
1471
+ reason: result.reason
1472
+ }
1473
+ });
1474
+ return outcome;
1475
+ }
1476
+ async run(sessionId, expectedEntryId) {
1477
+ if (this.state.phase !== "ready" || this.state.busy.includes(sessionId) || this.state.pending.some((plan2) => plan2.schemaVersion === 2 && plan2.sessionId === sessionId) || this.lifetime.signal.aborted) {
1478
+ return { sessionId, entryId: expectedEntryId, status: "pending", reason: "pending-result" };
1479
+ }
1480
+ this.publish({ busy: [...this.state.busy, sessionId], error: null });
1481
+ let plan;
1482
+ try {
1483
+ plan = archivePlanSchema.parse(value(await this.api.prepare({ action: "unarchive", sessionId, operationId: crypto.randomUUID() }, this.lifetime.signal)));
1484
+ if (plan.sessionId !== sessionId || plan.expected.entryId !== expectedEntryId) {
1485
+ const result = {
1486
+ operationId: plan.operationId,
1487
+ action: "unarchive",
1488
+ sessionId: plan.sessionId,
1489
+ entryId: expectedEntryId,
1490
+ status: "conflict",
1491
+ reason: "entry-changed"
1492
+ };
1493
+ return this.settle(plan, result);
1494
+ }
1495
+ this.pending([...this.state.pending, plan]);
1496
+ return this.settle(plan, value(await this.api.execute(plan, this.lifetime.signal)));
1497
+ } catch {
1498
+ if (plan) {
1499
+ try {
1500
+ const operation = value(await this.api.getOperation(plan.operationId, this.lifetime.signal));
1501
+ if (operation?.schemaVersion === 2 && operation.result) return this.settle(plan, operation.result);
1502
+ } catch {
1503
+ }
1504
+ }
1505
+ const outcome = {
1506
+ sessionId,
1507
+ entryId: expectedEntryId,
1508
+ status: plan ? "pending" : "rejected",
1509
+ reason: plan ? "pending-result" : "connection-failed"
1510
+ };
1511
+ this.publish({ error: outcome.reason, results: [outcome], notice: {
1512
+ sequence: ++this.noticeSequence,
1513
+ kind: "failed",
1514
+ sessionId,
1515
+ entryId: outcome.entryId,
1516
+ reason: outcome.reason
1517
+ } });
1518
+ return outcome;
1519
+ } finally {
1520
+ this.publish({ busy: this.state.busy.filter((id) => id !== sessionId) });
1521
+ }
1522
+ }
1523
+ };
1524
+ function browserPendingCache(storage) {
1525
+ const key2 = "dsh-session-bin.pending.v2";
1526
+ const legacyKey = "dsh-session-bin.pending.v1";
1527
+ const purgeKey = "dsh-session-bin.purge.pending.v2";
1528
+ const purgeGrantKey = "dsh-session-bin.purge.grants.v2";
1529
+ let purgeCacheBlocked = false;
1530
+ const emptyPurgeEnvelope = () => ({
1531
+ schemaVersion: 1,
1532
+ plans: [],
1533
+ grants: [],
1534
+ observationAttempts: [],
1535
+ batchOperations: []
1536
+ });
1537
+ const parse = (item) => {
1538
+ try {
1539
+ return pendingPlans(JSON.parse(item ?? "[]"));
1540
+ } catch {
1541
+ return [];
1542
+ }
1543
+ };
1544
+ const assertPurgeCacheWritable = (...arrays) => {
1545
+ if (arrays.some((array) => array.length > 64)) purgeCacheBlocked = true;
1546
+ if (purgeCacheBlocked) throw new Error("Deletion cache capacity exceeded.");
1547
+ };
1548
+ const writePurgeEnvelope = (envelope) => {
1549
+ assertPurgeCacheWritable(envelope.plans, envelope.grants, envelope.observationAttempts, envelope.batchOperations);
1550
+ storage.setItem(purgeKey, JSON.stringify(envelope));
1551
+ storage.removeItem(purgeGrantKey);
1552
+ };
1553
+ const readPurgeEnvelope = () => {
1554
+ const encoded = storage.getItem(purgeKey);
1555
+ let raw = null;
1556
+ try {
1557
+ raw = JSON.parse(encoded ?? "null");
1558
+ } catch {
1559
+ }
1560
+ const legacyEncoded = storage.getItem(purgeGrantKey);
1561
+ let rawLegacyGrants = [];
1562
+ try {
1563
+ rawLegacyGrants = JSON.parse(legacyEncoded ?? "[]");
1564
+ } catch {
1565
+ }
1566
+ const legacyArray = encoded !== null && Array.isArray(raw);
1567
+ const record = raw && typeof raw === "object" && !Array.isArray(raw) ? raw : null;
1568
+ const rawPlans = legacyArray ? raw : record?.schemaVersion === 1 ? record.plans : [];
1569
+ const rawGrants = record?.schemaVersion === 1 ? record.grants : [];
1570
+ const rawObservations = record?.schemaVersion === 1 ? record.observationAttempts : [];
1571
+ const rawBatchOperations = record?.schemaVersion === 1 ? record.batchOperations : [];
1572
+ if (purgeCacheOverflow(rawPlans, rawGrants, rawObservations, rawBatchOperations, rawLegacyGrants)) purgeCacheBlocked = true;
1573
+ let envelope = emptyPurgeEnvelope();
1574
+ if (legacyArray) envelope.plans = purgePlans(rawPlans);
1575
+ else if (record?.schemaVersion === 1) {
1576
+ envelope = {
1577
+ schemaVersion: 1,
1578
+ plans: purgePlans(rawPlans),
1579
+ grants: purgeGrantSnapshots(rawGrants),
1580
+ observationAttempts: purgeGrantObservations(rawObservations),
1581
+ batchOperations: purgeBatchOperations(rawBatchOperations)
1582
+ };
1583
+ }
1584
+ const activeIds = new Set(envelope.plans.map((plan) => plan.operationId));
1585
+ envelope.grants = purgeGrantSnapshots([...envelope.grants, ...purgeGrantSnapshots(rawLegacyGrants)]).filter((grant) => activeIds.has(grant.operationId));
1586
+ envelope.observationAttempts = purgeGrantObservations(envelope.observationAttempts).filter((observation) => activeIds.has(observation.operationId));
1587
+ envelope.batchOperations = purgeBatchOperations(envelope.batchOperations).filter((operationId) => activeIds.has(operationId));
1588
+ if (legacyArray) {
1589
+ const grantedIds = new Set(envelope.grants.map((grant) => grant.operationId));
1590
+ envelope.observationAttempts = purgeGrantObservations([
1591
+ ...envelope.observationAttempts,
1592
+ ...envelope.plans.filter((plan) => !grantedIds.has(plan.operationId)).map((plan) => ({ operationId: plan.operationId }))
1593
+ ]);
1594
+ }
1595
+ if (purgeCacheOverflow(envelope.plans, envelope.grants, envelope.observationAttempts, envelope.batchOperations)) purgeCacheBlocked = true;
1596
+ if (!purgeCacheBlocked && (legacyArray || legacyEncoded !== null)) {
1597
+ try {
1598
+ if (envelope.plans.length) writePurgeEnvelope(envelope);
1599
+ else {
1600
+ storage.removeItem(purgeGrantKey);
1601
+ storage.removeItem(purgeKey);
1602
+ }
1603
+ } catch {
1604
+ }
1605
+ }
1606
+ return envelope;
1607
+ };
1608
+ const save = (plans) => {
1609
+ if (plans.length) storage.setItem(key2, JSON.stringify(plans));
1610
+ else storage.removeItem(key2);
1611
+ storage.removeItem(legacyKey);
1612
+ };
1613
+ const savePurge = (plans, batchOperationIds = []) => {
1614
+ assertPurgeCacheWritable(plans, batchOperationIds);
1615
+ const snapshots = purgePlans(plans);
1616
+ const batchOperations = purgeBatchOperations(batchOperationIds);
1617
+ assertPurgeCacheWritable(snapshots, batchOperations);
1618
+ if (!snapshots.length) {
1619
+ storage.removeItem(purgeGrantKey);
1620
+ storage.removeItem(purgeKey);
1621
+ return;
1622
+ }
1623
+ const current = readPurgeEnvelope();
1624
+ assertPurgeCacheWritable(current.plans, current.grants, current.observationAttempts, current.batchOperations);
1625
+ const activeIds = new Set(snapshots.map((plan) => plan.operationId));
1626
+ writePurgeEnvelope({
1627
+ schemaVersion: 1,
1628
+ plans: snapshots,
1629
+ grants: current.grants.filter((grant) => activeIds.has(grant.operationId)),
1630
+ observationAttempts: current.observationAttempts.filter((observation) => activeIds.has(observation.operationId)),
1631
+ batchOperations: batchOperations.filter((operationId) => activeIds.has(operationId))
1632
+ });
1633
+ };
1634
+ return {
1635
+ load: () => {
1636
+ const plans = pendingPlans([...parse(storage.getItem(key2)), ...parse(storage.getItem(legacyKey))]);
1637
+ try {
1638
+ save(plans);
1639
+ } catch {
1640
+ }
1641
+ return plans;
1642
+ },
1643
+ save,
1644
+ loadPurge: () => readPurgeEnvelope().plans,
1645
+ savePurge,
1646
+ loadPurgeGrants: () => readPurgeEnvelope().grants,
1647
+ savePurgeGrants: (grants) => {
1648
+ assertPurgeCacheWritable(grants);
1649
+ const current = readPurgeEnvelope();
1650
+ assertPurgeCacheWritable(current.plans, current.grants, current.observationAttempts, current.batchOperations);
1651
+ const activeIds = new Set(current.plans.map((plan) => plan.operationId));
1652
+ writePurgeEnvelope({ ...current, grants: purgeGrantSnapshots(grants).filter((grant) => activeIds.has(grant.operationId)) });
1653
+ },
1654
+ loadPurgeGrantObservations: () => readPurgeEnvelope().observationAttempts,
1655
+ stagePurgeGrantObservation: (operationId) => {
1656
+ if (typeof operationId !== "string" || operationId.length === 0 || operationId.length > 1024) throw new Error("Invalid operation identity.");
1657
+ const current = readPurgeEnvelope();
1658
+ assertPurgeCacheWritable(current.plans, current.grants, current.observationAttempts, current.batchOperations);
1659
+ if (!current.plans.some((plan) => plan.operationId === operationId)) throw new Error("Cannot stage an observation without its pending plan.");
1660
+ writePurgeEnvelope({ ...current, observationAttempts: purgeGrantObservations([...current.observationAttempts, { operationId }]) });
1661
+ },
1662
+ loadPurgeBatchOperations: () => readPurgeEnvelope().batchOperations,
1663
+ isPurgeCacheBlocked: () => {
1664
+ readPurgeEnvelope();
1665
+ return purgeCacheBlocked;
1666
+ }
1667
+ };
1668
+ }
1669
+ export {
1670
+ SessionBinClientModel,
1671
+ browserPendingCache,
1672
+ retirementManifestDigest
1673
+ };