@songsid/agend 2.3.3-beta.4 → 2.3.3-beta.5
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/dist/agent-cli.js +13 -112
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +0 -1
- package/dist/agent-endpoint.js +1 -9
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/agent-whats-new.d.ts +3 -6
- package/dist/agent-whats-new.js +7 -18
- package/dist/agent-whats-new.js.map +1 -1
- package/dist/auth-api.d.ts +0 -7
- package/dist/auth-api.js +1 -15
- package/dist/auth-api.js.map +1 -1
- package/dist/backend/codex.d.ts +1 -0
- package/dist/backend/codex.js +106 -15
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/types.d.ts +17 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/bundled-skills.d.ts +4 -6
- package/dist/bundled-skills.js +10 -14
- package/dist/bundled-skills.js.map +1 -1
- package/dist/channel/mcp-server.js +5 -5
- package/dist/channel/mcp-server.js.map +1 -1
- package/dist/channel/mcp-tools.d.ts +1 -3
- package/dist/channel/mcp-tools.js +4 -58
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/cli.js +3 -1
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +6 -3
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +11 -3
- package/dist/daemon.js +59 -107
- package/dist/daemon.js.map +1 -1
- package/dist/dashboard-live.d.ts +1 -1
- package/dist/dashboard-live.js +3 -3
- package/dist/dashboard-live.js.map +1 -1
- package/dist/dashboard-permissions.d.ts +1 -1
- package/dist/dashboard-permissions.js +1 -3
- package/dist/dashboard-permissions.js.map +1 -1
- package/dist/fleet-manager.d.ts +26 -44
- package/dist/fleet-manager.js +70 -226
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/credential-profiles/SKILL.md +11 -1
- package/dist/general-knowledge/skills/session-management/SKILL.md +6 -0
- package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -204
- package/dist/general-knowledge/skills/web-dashboard/SKILL.md +26 -63
- package/dist/instance-lifecycle.js +1 -1
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +2 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.js +19 -7
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +0 -62
- package/dist/outbound-schemas.js +2 -41
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/public-web-gateway.js +0 -9
- package/dist/public-web-gateway.js.map +1 -1
- package/dist/public-web-link.d.ts +2 -0
- package/dist/public-web-link.js +5 -0
- package/dist/public-web-link.js.map +1 -1
- package/dist/search/search-docs.d.ts +1 -1
- package/dist/search/search-docs.js +2 -2
- package/dist/search/search-docs.js.map +1 -1
- package/dist/settings-api.d.ts +6 -0
- package/dist/settings-api.js +14 -2
- package/dist/settings-api.js.map +1 -1
- package/dist/settings-change.js +3 -1
- package/dist/settings-change.js.map +1 -1
- package/dist/tool-permissions.js +3 -10
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +6 -2
- package/dist/topic-commands.js +2 -0
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +2 -1
- package/dist/ui/chat-ambient.js +4 -4
- package/dist/ui/chat-commands.js +3 -2
- package/dist/ui/chat-store.js +4 -1
- package/dist/ui/chat-strings.js +6 -42
- package/dist/ui/chat-thread.js +6 -44
- package/dist/ui/panel-chat.js +21 -50
- package/dist/ui/preview-panel.js +3 -15
- package/dist/ui/search-dialog.js +30 -14
- package/dist/ui/settings-appearance.js +1 -3
- package/dist/ui/settings-guests.js +13 -4
- package/dist/ui/settings-learn.js +1 -1
- package/dist/ui/settings-mods.js +5 -12
- package/dist/ui/settings-registry.js +0 -1
- package/dist/ui/settings-strings.js +6 -6
- package/dist/ui/shared/app-prefs.js +17 -17
- package/dist/ui/shared/app-shell.js +12 -8
- package/dist/ui/shared/app-store.js +6 -2
- package/dist/ui/shared/app.css +4 -36
- package/dist/ui/shared/app.js +5 -4
- package/dist/ui/shared/instance-nav.js +9 -9
- package/dist/ui/shared/instance-switch.js +8 -2
- package/dist/ui/shared/panel-view.js +22 -8
- package/dist/ui/shared/view-strings.js +6 -0
- package/dist/usage/usage-api.d.ts +3 -0
- package/dist/usage/usage-api.js +5 -1
- package/dist/usage/usage-api.js.map +1 -1
- package/dist/view-api.d.ts +2 -0
- package/dist/view-api.js +64 -0
- package/dist/view-api.js.map +1 -1
- package/dist/web-api.d.ts +9 -17
- package/dist/web-api.js +255 -371
- package/dist/web-api.js.map +1 -1
- package/dist/web-appearance-tools.d.ts +0 -2
- package/dist/web-appearance-tools.js +2 -5
- package/dist/web-appearance-tools.js.map +1 -1
- package/dist/web-appearance-view.d.ts +0 -6
- package/dist/web-appearance-view.js +0 -1
- package/dist/web-appearance-view.js.map +1 -1
- package/dist/web-appearance.d.ts +14 -7
- package/dist/web-appearance.js +5 -6
- package/dist/web-appearance.js.map +1 -1
- package/dist/web-asset-parts.d.ts +1 -17
- package/dist/web-asset-parts.js +1 -15
- package/dist/web-asset-parts.js.map +1 -1
- package/dist/web-auth.js +1 -7
- package/dist/web-auth.js.map +1 -1
- package/dist/web-chat-history.d.ts +3 -7
- package/dist/web-chat-history.js +10 -17
- package/dist/web-chat-history.js.map +1 -1
- package/dist/web-commands.d.ts +9 -1
- package/dist/web-commands.js +13 -0
- package/dist/web-commands.js.map +1 -1
- package/dist/web-experimental.d.ts +2 -11
- package/dist/web-experimental.js +8 -36
- package/dist/web-experimental.js.map +1 -1
- package/dist/web-guest-access.d.ts +40 -2
- package/dist/web-guest-access.js +50 -14
- package/dist/web-guest-access.js.map +1 -1
- package/dist/web-mod-bundle.d.ts +2 -45
- package/dist/web-mod-bundle.js +15 -63
- package/dist/web-mod-bundle.js.map +1 -1
- package/dist/web-mod-store.d.ts +3 -48
- package/dist/web-mod-store.js +30 -97
- package/dist/web-mod-store.js.map +1 -1
- package/dist/web-prefs.d.ts +13 -0
- package/dist/web-prefs.js +38 -0
- package/dist/web-prefs.js.map +1 -1
- package/dist/web-preview.d.ts +1 -11
- package/dist/web-preview.js +6 -20
- package/dist/web-preview.js.map +1 -1
- package/dist/web-session.d.ts +11 -3
- package/dist/web-session.js +22 -11
- package/dist/web-session.js.map +1 -1
- package/dist/web-settings-patch.d.ts +1 -0
- package/dist/web-settings-patch.js +6 -3
- package/dist/web-settings-patch.js.map +1 -1
- package/dist/web-settings-tools.d.ts +0 -13
- package/dist/web-settings-tools.js +12 -84
- package/dist/web-settings-tools.js.map +1 -1
- package/package.json +1 -1
- package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +0 -376
- package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +0 -672
- package/dist/ui/app-effects.js +0 -251
- package/dist/ui/chat-effect-posters.js +0 -42
- package/dist/ui/chat-effects.js +0 -167
- package/dist/ui/effect-trial-controller.js +0 -124
- package/dist/ui/effect-trial-offer.js +0 -2
- package/dist/ui/effect-trial-panel.js +0 -40
- package/dist/ui/effects-waker.js +0 -2
- package/dist/ui/settings-effects.js +0 -202
- package/dist/ui/settings-owner-effects.js +0 -82
- package/dist/ui/shared/effect-app-runtime.js +0 -299
- package/dist/ui/shared/effect-baseline.js +0 -64
- package/dist/ui/shared/effect-chat-runtime.js +0 -162
- package/dist/ui/shared/effect-cursor.js +0 -126
- package/dist/ui/shared/effect-display-metadata.js +0 -39
- package/dist/ui/shared/effect-frame.js +0 -146
- package/dist/ui/shared/effect-geometry.js +0 -137
- package/dist/ui/shared/effect-host-style.js +0 -10
- package/dist/ui/shared/effect-owner-regions.js +0 -74
- package/dist/ui/shared/effect-owner-source.js +0 -122
- package/dist/ui/shared/effect-policy.js +0 -151
- package/dist/ui/shared/effect-poster-reader.js +0 -98
- package/dist/ui/shared/effect-posters.js +0 -117
- package/dist/ui/shared/effect-profiler.js +0 -184
- package/dist/ui/shared/effect-profiles.js +0 -65
- package/dist/ui/shared/effect-registry.js +0 -545
- package/dist/ui/shared/effect-runtime-loader.js +0 -236
- package/dist/ui/shared/effect-safety.js +0 -153
- package/dist/ui/shared/effect-stock-snapshot.js +0 -43
- package/dist/ui/shared/effect-stock-trust.js +0 -217
- package/dist/ui/shared/effect-trial-shadow.js +0 -62
- package/dist/ui/shared/effect-trial-source.js +0 -103
- package/dist/ui/shared/effect-trial-stage.js +0 -170
- package/dist/ui/shared/effect-trial.css +0 -40
- package/dist/ui/shared/feature-flags.js +0 -127
- package/dist/ui/shared/owner-effect-prefs.js +0 -117
- package/dist/web-effect-bootstrap.d.ts +0 -12
- package/dist/web-effect-bootstrap.js +0 -49
- package/dist/web-effect-bootstrap.js.map +0 -1
- package/dist/web-effect-bundle.d.ts +0 -28
- package/dist/web-effect-bundle.js +0 -171
- package/dist/web-effect-bundle.js.map +0 -1
- package/dist/web-effect-canonical.d.ts +0 -8
- package/dist/web-effect-canonical.js +0 -27
- package/dist/web-effect-canonical.js.map +0 -1
- package/dist/web-effect-library.d.ts +0 -114
- package/dist/web-effect-library.js +0 -530
- package/dist/web-effect-library.js.map +0 -1
- package/dist/web-effect-playback.d.ts +0 -30
- package/dist/web-effect-playback.js +0 -71
- package/dist/web-effect-playback.js.map +0 -1
- package/dist/web-effect-poster.d.ts +0 -8
- package/dist/web-effect-poster.js +0 -52
- package/dist/web-effect-poster.js.map +0 -1
- package/dist/web-effect-profiles.d.ts +0 -118
- package/dist/web-effect-profiles.js +0 -181
- package/dist/web-effect-profiles.js.map +0 -1
- package/dist/web-effect-service.d.ts +0 -93
- package/dist/web-effect-service.js +0 -335
- package/dist/web-effect-service.js.map +0 -1
- package/dist/web-effect-tools.d.ts +0 -48
- package/dist/web-effect-tools.js +0 -129
- package/dist/web-effect-tools.js.map +0 -1
- package/dist/web-effect-trials.d.ts +0 -86
- package/dist/web-effect-trials.js +0 -204
- package/dist/web-effect-trials.js.map +0 -1
- package/dist/web-effect-upload.d.ts +0 -27
- package/dist/web-effect-upload.js +0 -185
- package/dist/web-effect-upload.js.map +0 -1
- package/dist/web-effect-worker.d.ts +0 -8
- package/dist/web-effect-worker.js +0 -170
- package/dist/web-effect-worker.js.map +0 -1
- package/dist/web-effect.d.ts +0 -103
- package/dist/web-effect.js +0 -153
- package/dist/web-effect.js.map +0 -1
- package/dist/web-experimental-http.d.ts +0 -2
- package/dist/web-experimental-http.js +0 -12
- package/dist/web-experimental-http.js.map +0 -1
- package/dist/web-mod-agent.d.ts +0 -50
- package/dist/web-mod-agent.js +0 -207
- package/dist/web-mod-agent.js.map +0 -1
|
@@ -1,114 +0,0 @@
|
|
|
1
|
-
import { type CheckedWebEffect } from "./web-effect-bundle.js";
|
|
2
|
-
import { type WebEffectManifest } from "./web-effect.js";
|
|
3
|
-
export interface EffectPrincipal {
|
|
4
|
-
kind: "agent" | "human";
|
|
5
|
-
id: string;
|
|
6
|
-
}
|
|
7
|
-
export interface EffectReference {
|
|
8
|
-
id: string;
|
|
9
|
-
holder: string;
|
|
10
|
-
}
|
|
11
|
-
export interface EffectSummary {
|
|
12
|
-
id: string;
|
|
13
|
-
manifest: WebEffectManifest;
|
|
14
|
-
bytes: number;
|
|
15
|
-
}
|
|
16
|
-
/** Caller-scoped controls; never includes another principal or any holder. */
|
|
17
|
-
export interface EffectManagementSummary extends EffectSummary {
|
|
18
|
-
ownClaim: boolean;
|
|
19
|
-
sharedByYou: boolean;
|
|
20
|
-
}
|
|
21
|
-
export type EffectFiles = ReadonlyMap<string, Uint8Array>;
|
|
22
|
-
type Failure = {
|
|
23
|
-
ok: false;
|
|
24
|
-
error: string;
|
|
25
|
-
};
|
|
26
|
-
type WriteReceipt = {
|
|
27
|
-
ok: true;
|
|
28
|
-
undo: () => boolean;
|
|
29
|
-
};
|
|
30
|
-
export interface EffectStageSummary extends EffectSummary {
|
|
31
|
-
warnings: string[];
|
|
32
|
-
}
|
|
33
|
-
export declare class WebEffectLibrary {
|
|
34
|
-
private readonly dataDir;
|
|
35
|
-
private readonly options;
|
|
36
|
-
readonly path: string;
|
|
37
|
-
private readonly blobs;
|
|
38
|
-
private readonly stages;
|
|
39
|
-
private readonly providers;
|
|
40
|
-
private busy;
|
|
41
|
-
private readingReferences;
|
|
42
|
-
constructor(dataDir: string, options?: {
|
|
43
|
-
monotonic?: () => number;
|
|
44
|
-
now?: () => number;
|
|
45
|
-
beforeIndexRename?: () => void;
|
|
46
|
-
});
|
|
47
|
-
private clock;
|
|
48
|
-
private now;
|
|
49
|
-
private load;
|
|
50
|
-
private save;
|
|
51
|
-
private sweep;
|
|
52
|
-
private claim;
|
|
53
|
-
private visible;
|
|
54
|
-
private quota;
|
|
55
|
-
private summary;
|
|
56
|
-
private error;
|
|
57
|
-
private transaction;
|
|
58
|
-
/** Host-only provider; unreadable/invalid providers never authorize deletion or reference reads. */
|
|
59
|
-
registerReferenceSource(name: string, source: () => Iterable<EffectReference>): () => void;
|
|
60
|
-
private references;
|
|
61
|
-
private referenceKey;
|
|
62
|
-
private guardReferences;
|
|
63
|
-
private readRow;
|
|
64
|
-
private writeBlob;
|
|
65
|
-
/** No writes. A restart loses the stage; callers must restage and review. */
|
|
66
|
-
stage(input: EffectFiles | readonly EffectFiles[], principal: EffectPrincipal, options?: {
|
|
67
|
-
holder?: string;
|
|
68
|
-
}): {
|
|
69
|
-
ok: true;
|
|
70
|
-
stageId: string;
|
|
71
|
-
ids: string[];
|
|
72
|
-
entries: EffectStageSummary[];
|
|
73
|
-
} | Failure;
|
|
74
|
-
abort(stageId: string): void;
|
|
75
|
-
private undo;
|
|
76
|
-
commit(stageId: string): (WriteReceipt & {
|
|
77
|
-
ids: string[];
|
|
78
|
-
created: string[];
|
|
79
|
-
}) | Failure;
|
|
80
|
-
list(principal: EffectPrincipal): {
|
|
81
|
-
ok: true;
|
|
82
|
-
entries: EffectSummary[];
|
|
83
|
-
} | Failure;
|
|
84
|
-
managementList(principal: EffectPrincipal): {
|
|
85
|
-
ok: true;
|
|
86
|
-
entries: EffectManagementSummary[];
|
|
87
|
-
} | Failure;
|
|
88
|
-
read(id: string, principal: EffectPrincipal): {
|
|
89
|
-
ok: true;
|
|
90
|
-
entry: CheckedWebEffect;
|
|
91
|
-
} | Failure;
|
|
92
|
-
/** Internal: caller first authorizes its own resource. Knowing an id/holder is never a route capability. */
|
|
93
|
-
readReferenced(id: string, holder: string): {
|
|
94
|
-
ok: true;
|
|
95
|
-
entry: CheckedWebEffect;
|
|
96
|
-
} | Failure;
|
|
97
|
-
setShared(id: string, principal: EffectPrincipal, shared: boolean): {
|
|
98
|
-
ok: true;
|
|
99
|
-
} | Failure;
|
|
100
|
-
/** Explicit normal delete removes this principal's whole unused claim, including orphan mod holders. */
|
|
101
|
-
deleteClaim(id: string, principal: EffectPrincipal): {
|
|
102
|
-
ok: true;
|
|
103
|
-
} | Failure;
|
|
104
|
-
/** Host-only mod uninstall. Never releases a direct upload or another holder/principal. */
|
|
105
|
-
releaseHolder(holder: string, principal: EffectPrincipal): (WriteReceipt & {
|
|
106
|
-
released: string[];
|
|
107
|
-
}) | Failure;
|
|
108
|
-
/** Host-only notification after a placement/reference is removed; never a caller capability. */
|
|
109
|
-
collectUnreferenced(ids: readonly string[]): void;
|
|
110
|
-
private collectLater;
|
|
111
|
-
}
|
|
112
|
-
/** r4-5 uses this structural interface; it need not depend on the concrete store in tests. */
|
|
113
|
-
export type EffectLibraryParticipant = Pick<WebEffectLibrary, "stage" | "commit" | "abort" | "read" | "readReferenced" | "releaseHolder" | "registerReferenceSource">;
|
|
114
|
-
export {};
|
|
@@ -1,530 +0,0 @@
|
|
|
1
|
-
/** #1629 CW1 library. Single fleet owner; stages are volatile, claims durable and scoped. */
|
|
2
|
-
import { randomBytes, createHash } from "node:crypto";
|
|
3
|
-
import { constants, openSync, fstatSync, readSync, closeSync, mkdirSync, mkdtempSync, writeFileSync, fsyncSync, renameSync, rmSync } from "node:fs";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { performance } from "node:perf_hooks";
|
|
6
|
-
import { createHook } from "node:async_hooks";
|
|
7
|
-
import { z } from "zod";
|
|
8
|
-
import { atomicWriteFileSync } from "./atomic-write.js";
|
|
9
|
-
import { checkEffectEntry } from "./web-effect-bundle.js";
|
|
10
|
-
import { effectManifestJson } from "./web-effect-canonical.js";
|
|
11
|
-
import { parseWebEffectManifest } from "./web-effect.js";
|
|
12
|
-
const ID = /^[0-9a-f]{32}$/;
|
|
13
|
-
const MOD = /^mod:[0-9a-f]{32}$/;
|
|
14
|
-
const PATH = /^(?:effect\.json|worker\.js|poster\.png|cursor\.png|assets\/[a-z0-9][a-z0-9_-]{0,63}\.(?:png|jpe?g|webp|gif))$/;
|
|
15
|
-
const INDEX_LIMIT = 4 * 1024 * 1024;
|
|
16
|
-
const STAGE_TTL = 10 * 60 * 1000;
|
|
17
|
-
const STAGE_MEMORY = 64 * 1024 * 1024;
|
|
18
|
-
const QUOTA_BYTES = 8 * 1024 * 1024;
|
|
19
|
-
const principalSchema = z.strictObject({ kind: z.enum(["agent", "human"]), id: z.string().min(1).max(256) });
|
|
20
|
-
const claimSchema = z.strictObject({ principal: principalSchema, holders: z.array(z.string().refine(v => v === "upload" || MOD.test(v))).min(1).max(64), uploaded: z.number().finite().nonnegative(), shared: z.boolean() });
|
|
21
|
-
const rowSchema = z.strictObject({ sha256: z.string().regex(/^[0-9a-f]{64}$/), bytes: z.number().int().min(1).max(1024 * 1024), manifest: z.unknown(), paths: z.array(z.string().regex(PATH)).min(2).max(20), claims: z.array(claimSchema).max(256) });
|
|
22
|
-
const documentSchema = z.strictObject({ version: z.literal(1), rev: z.number().int().nonnegative().max(Number.MAX_SAFE_INTEGER - 1), entries: z.record(z.string().regex(ID), rowSchema) });
|
|
23
|
-
const clone = (v) => JSON.parse(JSON.stringify(v));
|
|
24
|
-
const key = (p) => JSON.stringify([p.kind, p.id]);
|
|
25
|
-
const digest = (s) => createHash("sha256").update(s).digest("hex");
|
|
26
|
-
const empty = () => ({ version: 1, rev: 0, entries: Object.create(null) });
|
|
27
|
-
const fail = (error) => ({ ok: false, error });
|
|
28
|
-
class StoreError extends Error {
|
|
29
|
-
code;
|
|
30
|
-
constructor(code) {
|
|
31
|
-
super(code);
|
|
32
|
-
this.code = code;
|
|
33
|
-
}
|
|
34
|
-
}
|
|
35
|
-
/** Read a regular file through one fd with a physical cap, including growth after stat. */
|
|
36
|
-
function boundedRead(path, cap) {
|
|
37
|
-
const fd = openSync(path, constants.O_RDONLY | (constants.O_NOFOLLOW ?? 0));
|
|
38
|
-
try {
|
|
39
|
-
const info = fstatSync(fd);
|
|
40
|
-
if (!info.isFile() || info.size > cap)
|
|
41
|
-
throw new StoreError("file_bound");
|
|
42
|
-
const buffer = Buffer.alloc(info.size + 1);
|
|
43
|
-
let n = 0;
|
|
44
|
-
while (n < buffer.length) {
|
|
45
|
-
const got = readSync(fd, buffer, n, buffer.length - n, null);
|
|
46
|
-
if (!got)
|
|
47
|
-
break;
|
|
48
|
-
n += got;
|
|
49
|
-
}
|
|
50
|
-
if (n !== info.size || fstatSync(fd).size !== info.size)
|
|
51
|
-
throw new StoreError("file_changed");
|
|
52
|
-
return buffer.subarray(0, n);
|
|
53
|
-
}
|
|
54
|
-
finally {
|
|
55
|
-
closeSync(fd);
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
function syncDirectory(path) {
|
|
59
|
-
let fd;
|
|
60
|
-
try {
|
|
61
|
-
fd = openSync(path, "r");
|
|
62
|
-
fsyncSync(fd);
|
|
63
|
-
}
|
|
64
|
-
catch { /* directory fsync is best effort, as in atomic-write.ts */ }
|
|
65
|
-
finally {
|
|
66
|
-
if (fd !== undefined)
|
|
67
|
-
closeSync(fd);
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
export class WebEffectLibrary {
|
|
71
|
-
dataDir;
|
|
72
|
-
options;
|
|
73
|
-
path;
|
|
74
|
-
blobs;
|
|
75
|
-
stages = new Map();
|
|
76
|
-
providers = new Map();
|
|
77
|
-
busy = false;
|
|
78
|
-
readingReferences = false;
|
|
79
|
-
constructor(dataDir, options = {}) {
|
|
80
|
-
this.dataDir = dataDir;
|
|
81
|
-
this.options = options;
|
|
82
|
-
this.path = join(dataDir, "web-effects.json");
|
|
83
|
-
this.blobs = join(dataDir, "web-effects");
|
|
84
|
-
}
|
|
85
|
-
clock() {
|
|
86
|
-
const value = (this.options.monotonic ?? (() => performance.now()))();
|
|
87
|
-
if (!Number.isFinite(value))
|
|
88
|
-
throw new StoreError("clock_unavailable");
|
|
89
|
-
return value;
|
|
90
|
-
}
|
|
91
|
-
now() {
|
|
92
|
-
const value = (this.options.now ?? Date.now)();
|
|
93
|
-
if (!Number.isFinite(value) || value < 0)
|
|
94
|
-
throw new StoreError("clock_unavailable");
|
|
95
|
-
return value;
|
|
96
|
-
}
|
|
97
|
-
load() {
|
|
98
|
-
let bytes;
|
|
99
|
-
try {
|
|
100
|
-
bytes = boundedRead(this.path, INDEX_LIMIT);
|
|
101
|
-
}
|
|
102
|
-
catch (error) {
|
|
103
|
-
if (error.code === "ENOENT")
|
|
104
|
-
return { document: empty(), fingerprint: null };
|
|
105
|
-
throw new StoreError("library_unreadable");
|
|
106
|
-
}
|
|
107
|
-
try {
|
|
108
|
-
const input = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
|
|
109
|
-
if (!input || typeof input !== "object" || !Object.hasOwn(input, "entries"))
|
|
110
|
-
throw new Error("invalid document");
|
|
111
|
-
const rows = input.entries;
|
|
112
|
-
if (!rows || typeof rows !== "object" || Array.isArray(rows) || Object.keys(rows).length > 1024)
|
|
113
|
-
throw new Error("index bound");
|
|
114
|
-
for (const value of Object.values(rows)) {
|
|
115
|
-
if (!value || typeof value !== "object")
|
|
116
|
-
throw new Error("invalid row");
|
|
117
|
-
const row = value;
|
|
118
|
-
if (!Array.isArray(row.claims) || row.claims.length > 256 || !Array.isArray(row.paths) || row.paths.length > 20)
|
|
119
|
-
throw new Error("row bound");
|
|
120
|
-
}
|
|
121
|
-
const decoded = documentSchema.parse(input);
|
|
122
|
-
if (Object.keys(decoded.entries).length > 1024)
|
|
123
|
-
throw new Error("index bound");
|
|
124
|
-
const entries = Object.create(null);
|
|
125
|
-
for (const [id, row] of Object.entries(decoded.entries)) {
|
|
126
|
-
const manifest = parseWebEffectManifest(row.manifest);
|
|
127
|
-
if (!manifest.ok || !row.sha256.startsWith(id) || new Set(row.paths).size !== row.paths.length || !row.paths.includes("effect.json") || !row.paths.includes("worker.js"))
|
|
128
|
-
throw new Error("invalid entry");
|
|
129
|
-
const owners = new Set();
|
|
130
|
-
for (const claim of row.claims) {
|
|
131
|
-
const owner = key(claim.principal);
|
|
132
|
-
if (owners.has(owner) || new Set(claim.holders).size !== claim.holders.length || claim.shared && claim.principal.kind !== "human")
|
|
133
|
-
throw new Error("invalid claim");
|
|
134
|
-
owners.add(owner);
|
|
135
|
-
}
|
|
136
|
-
entries[id] = { ...row, manifest: manifest.manifest };
|
|
137
|
-
}
|
|
138
|
-
return { document: { ...decoded, entries }, fingerprint: digest(bytes) };
|
|
139
|
-
}
|
|
140
|
-
catch {
|
|
141
|
-
throw new StoreError("library_unreadable");
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
save(before, document, validate) {
|
|
145
|
-
if (before.document.rev >= Number.MAX_SAFE_INTEGER - 1)
|
|
146
|
-
throw new StoreError("revision_exhausted");
|
|
147
|
-
document.rev = before.document.rev + 1;
|
|
148
|
-
const text = JSON.stringify(document) + "\n";
|
|
149
|
-
if (Buffer.byteLength(text) > INDEX_LIMIT || Object.keys(document.entries).length > 1024)
|
|
150
|
-
throw new StoreError("library_full");
|
|
151
|
-
mkdirSync(this.dataDir, { recursive: true, mode: 0o700 });
|
|
152
|
-
atomicWriteFileSync(this.path, text, { mode: 0o600, beforeRename: () => {
|
|
153
|
-
this.options.beforeIndexRename?.();
|
|
154
|
-
if (this.load().fingerprint !== before.fingerprint)
|
|
155
|
-
throw new StoreError("library_changed");
|
|
156
|
-
validate?.();
|
|
157
|
-
} });
|
|
158
|
-
return { document, fingerprint: digest(text) };
|
|
159
|
-
}
|
|
160
|
-
sweep() {
|
|
161
|
-
const now = this.clock(), released = [];
|
|
162
|
-
for (const [id, stage] of this.stages)
|
|
163
|
-
if (now >= stage.expires) {
|
|
164
|
-
this.stages.delete(id);
|
|
165
|
-
released.push(...stage.ids);
|
|
166
|
-
}
|
|
167
|
-
if (released.length)
|
|
168
|
-
this.collectLater(released);
|
|
169
|
-
}
|
|
170
|
-
claim(row, principal) { return row.claims.find(c => key(c.principal) === key(principal)); }
|
|
171
|
-
visible(row, principal) { return !!this.claim(row, principal) || row.claims.some(c => c.principal.kind === "human" && c.shared); }
|
|
172
|
-
quota(document, entries, principal) {
|
|
173
|
-
const own = new Map(Object.entries(document.entries).filter(([, r]) => this.claim(r, principal)).map(([id, r]) => [id, r.bytes]));
|
|
174
|
-
for (const entry of entries)
|
|
175
|
-
own.set(entry.id, entry.bytes);
|
|
176
|
-
if (own.size > 10 || [...own.values()].reduce((a, b) => a + b, 0) > QUOTA_BYTES)
|
|
177
|
-
throw new StoreError("effect_quota");
|
|
178
|
-
}
|
|
179
|
-
summary(id, row) { return { id, manifest: clone(row.manifest), bytes: row.bytes }; }
|
|
180
|
-
error(error) { return fail(error instanceof StoreError ? error.code : "library_write_failed"); }
|
|
181
|
-
transaction(run) {
|
|
182
|
-
if (this.busy || this.readingReferences)
|
|
183
|
-
return fail("library_busy");
|
|
184
|
-
this.busy = true;
|
|
185
|
-
try {
|
|
186
|
-
return run();
|
|
187
|
-
}
|
|
188
|
-
catch (error) {
|
|
189
|
-
return this.error(error);
|
|
190
|
-
}
|
|
191
|
-
finally {
|
|
192
|
-
this.busy = false;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
/** Host-only provider; unreadable/invalid providers never authorize deletion or reference reads. */
|
|
196
|
-
registerReferenceSource(name, source) {
|
|
197
|
-
if (!name || name.length > 64 || typeof source !== "function" || this.providers.size >= 64 || this.providers.has(name))
|
|
198
|
-
throw new Error("Duplicate reference source");
|
|
199
|
-
this.providers.set(name, source);
|
|
200
|
-
return () => {
|
|
201
|
-
if (this.providers.get(name) === source)
|
|
202
|
-
this.providers.delete(name);
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
references() {
|
|
206
|
-
if (this.readingReferences)
|
|
207
|
-
throw new StoreError("references_unreadable");
|
|
208
|
-
this.readingReferences = true;
|
|
209
|
-
try {
|
|
210
|
-
const sources = [...this.providers], refs = [];
|
|
211
|
-
for (const [, source] of sources)
|
|
212
|
-
for (const ref of source()) {
|
|
213
|
-
if (refs.length >= 4096 || !ref || !ID.test(ref.id) || typeof ref.holder !== "string" || ref.holder.length < 1 || ref.holder.length > 256)
|
|
214
|
-
throw new Error("invalid reference");
|
|
215
|
-
refs.push({ id: ref.id, holder: ref.holder });
|
|
216
|
-
}
|
|
217
|
-
if (sources.length !== this.providers.size || sources.some(([name, fn]) => this.providers.get(name) !== fn))
|
|
218
|
-
throw new Error("provider changed");
|
|
219
|
-
return refs;
|
|
220
|
-
}
|
|
221
|
-
catch {
|
|
222
|
-
throw new StoreError("references_unreadable");
|
|
223
|
-
}
|
|
224
|
-
finally {
|
|
225
|
-
this.readingReferences = false;
|
|
226
|
-
}
|
|
227
|
-
}
|
|
228
|
-
referenceKey(ids, refs) {
|
|
229
|
-
return JSON.stringify(refs.filter(r => ids.includes(r.id)).map(r => JSON.stringify([r.id, r.holder])).sort());
|
|
230
|
-
}
|
|
231
|
-
guardReferences(ids, refs) {
|
|
232
|
-
const expected = this.referenceKey(ids, refs);
|
|
233
|
-
return () => {
|
|
234
|
-
if (this.referenceKey(ids, this.references()) !== expected)
|
|
235
|
-
throw new StoreError("references_changed");
|
|
236
|
-
};
|
|
237
|
-
}
|
|
238
|
-
readRow(id, row) {
|
|
239
|
-
let size = 0;
|
|
240
|
-
const files = new Map();
|
|
241
|
-
for (const path of row.paths) {
|
|
242
|
-
const bytes = boundedRead(join(this.blobs, id, path), 1024 * 1024 - size);
|
|
243
|
-
size += bytes.length;
|
|
244
|
-
files.set(path, bytes);
|
|
245
|
-
}
|
|
246
|
-
const checked = checkEffectEntry(files);
|
|
247
|
-
if (!checked.ok || checked.id !== id || checked.sha256 !== row.sha256 || checked.bytes !== row.bytes || effectManifestJson(checked.manifest) !== effectManifestJson(row.manifest))
|
|
248
|
-
throw new StoreError("blob_unreadable");
|
|
249
|
-
return checked;
|
|
250
|
-
}
|
|
251
|
-
writeBlob(entry, existing) {
|
|
252
|
-
if (existing) {
|
|
253
|
-
if (existing.sha256 !== entry.sha256)
|
|
254
|
-
throw new StoreError("hash_conflict");
|
|
255
|
-
this.readRow(entry.id, existing);
|
|
256
|
-
return;
|
|
257
|
-
}
|
|
258
|
-
mkdirSync(this.blobs, { recursive: true, mode: 0o700 });
|
|
259
|
-
// A complete orphan may be reused only after this upload supplied and validated the same full content.
|
|
260
|
-
try {
|
|
261
|
-
this.readRow(entry.id, { ...entry, paths: [...entry.files.keys()], claims: [] });
|
|
262
|
-
return;
|
|
263
|
-
}
|
|
264
|
-
catch (error) {
|
|
265
|
-
if (error.code !== "ENOENT")
|
|
266
|
-
throw new StoreError("blob_conflict");
|
|
267
|
-
}
|
|
268
|
-
const temp = mkdtempSync(join(this.blobs, ".stage-"));
|
|
269
|
-
try {
|
|
270
|
-
for (const [path, bytes] of entry.files) {
|
|
271
|
-
if (path.startsWith("assets/"))
|
|
272
|
-
mkdirSync(join(temp, "assets"), { recursive: true, mode: 0o700 });
|
|
273
|
-
const fd = openSync(join(temp, path), "wx", 0o600);
|
|
274
|
-
try {
|
|
275
|
-
writeFileSync(fd, bytes);
|
|
276
|
-
fsyncSync(fd);
|
|
277
|
-
}
|
|
278
|
-
finally {
|
|
279
|
-
closeSync(fd);
|
|
280
|
-
}
|
|
281
|
-
}
|
|
282
|
-
if ([...entry.files.keys()].some(path => path.startsWith("assets/")))
|
|
283
|
-
syncDirectory(join(temp, "assets"));
|
|
284
|
-
syncDirectory(temp);
|
|
285
|
-
renameSync(temp, join(this.blobs, entry.id));
|
|
286
|
-
syncDirectory(this.blobs);
|
|
287
|
-
}
|
|
288
|
-
finally {
|
|
289
|
-
rmSync(temp, { recursive: true, force: true });
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
/** No writes. A restart loses the stage; callers must restage and review. */
|
|
293
|
-
stage(input, principal, options = {}) {
|
|
294
|
-
return this.transaction(() => {
|
|
295
|
-
if (!principalSchema.safeParse(principal).success || options.holder !== undefined && !MOD.test(options.holder))
|
|
296
|
-
return fail("invalid_principal");
|
|
297
|
-
this.sweep();
|
|
298
|
-
const inputs = input instanceof Map ? [input] : input;
|
|
299
|
-
if (!Array.isArray(inputs) || inputs.length < 1 || inputs.length > 10)
|
|
300
|
-
return fail("invalid_entries");
|
|
301
|
-
let raw = 0, members = 0;
|
|
302
|
-
for (const files of inputs) {
|
|
303
|
-
if (++members > 10)
|
|
304
|
-
return fail("invalid_entries");
|
|
305
|
-
if (!(files instanceof Map) || files.size > 20)
|
|
306
|
-
return fail("invalid_entries");
|
|
307
|
-
let parts = 0;
|
|
308
|
-
for (const bytes of files.values()) {
|
|
309
|
-
if (++parts > 20 || !(bytes instanceof Uint8Array))
|
|
310
|
-
return fail("invalid_entries");
|
|
311
|
-
raw += bytes.byteLength;
|
|
312
|
-
if (raw > QUOTA_BYTES)
|
|
313
|
-
return fail("stage_too_large");
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
const unique = new Map(), ids = [];
|
|
317
|
-
for (const files of inputs) {
|
|
318
|
-
const entry = checkEffectEntry(files);
|
|
319
|
-
if (!entry.ok)
|
|
320
|
-
return fail("invalid_entry");
|
|
321
|
-
if (unique.has(entry.id) && unique.get(entry.id).sha256 !== entry.sha256)
|
|
322
|
-
return fail("hash_conflict");
|
|
323
|
-
unique.set(entry.id, entry);
|
|
324
|
-
ids.push(entry.id);
|
|
325
|
-
}
|
|
326
|
-
const entries = [...unique.values()], bytes = entries.reduce((sum, e) => sum + e.bytes, 0);
|
|
327
|
-
this.quota(this.load().document, entries, principal);
|
|
328
|
-
if (this.stages.size >= 64 || [...this.stages.values()].filter(s => key(s.principal) === key(principal)).length >= 4 || [...this.stages.values()].reduce((sum, s) => sum + s.bytes, bytes) > STAGE_MEMORY)
|
|
329
|
-
return fail("stage_full");
|
|
330
|
-
const stageId = randomBytes(16).toString("hex");
|
|
331
|
-
this.stages.set(stageId, { principal: clone(principal), holder: options.holder ?? "upload", expires: this.clock() + STAGE_TTL, ids, entries, bytes });
|
|
332
|
-
return { ok: true, stageId, ids: [...ids], entries: ids.map(id => ({ ...this.summary(id, unique.get(id)), warnings: [...unique.get(id).warnings] })) };
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
abort(stageId) {
|
|
336
|
-
const stage = this.stages.get(stageId);
|
|
337
|
-
this.stages.delete(stageId);
|
|
338
|
-
if (stage)
|
|
339
|
-
this.collectLater(stage.ids);
|
|
340
|
-
}
|
|
341
|
-
undo(before, after, ids, refs) {
|
|
342
|
-
let active = true;
|
|
343
|
-
// Expire BEFORE the next async callback, even one queued before this receipt existed.
|
|
344
|
-
// A queued expiry alone is too late for prequeued microtasks. This temporary hook
|
|
345
|
-
// disables itself at the first callback (or synchronous undo), with a cleanup microtask.
|
|
346
|
-
const expire = () => { active = false; boundary.disable(); };
|
|
347
|
-
const boundary = createHook({ before: expire });
|
|
348
|
-
boundary.enable();
|
|
349
|
-
queueMicrotask(expire);
|
|
350
|
-
const relevant = (rows) => this.referenceKey(ids, rows);
|
|
351
|
-
return () => {
|
|
352
|
-
if (!active)
|
|
353
|
-
return false;
|
|
354
|
-
expire();
|
|
355
|
-
const result = this.transaction(() => {
|
|
356
|
-
const current = this.load();
|
|
357
|
-
if (current.fingerprint !== after.fingerprint || relevant(this.references()) !== relevant(refs))
|
|
358
|
-
return false;
|
|
359
|
-
this.save(current, clone(before.document), this.guardReferences(ids, refs));
|
|
360
|
-
this.collectLater(ids);
|
|
361
|
-
return true;
|
|
362
|
-
});
|
|
363
|
-
return result === true;
|
|
364
|
-
};
|
|
365
|
-
}
|
|
366
|
-
commit(stageId) {
|
|
367
|
-
return this.transaction(() => {
|
|
368
|
-
this.sweep();
|
|
369
|
-
const stage = this.stages.get(stageId);
|
|
370
|
-
if (!stage)
|
|
371
|
-
return fail("unknown_stage");
|
|
372
|
-
const before = this.load(), refs = this.references(), document = clone(before.document), created = [];
|
|
373
|
-
this.quota(document, stage.entries, stage.principal);
|
|
374
|
-
for (const entry of stage.entries) {
|
|
375
|
-
const old = Object.hasOwn(document.entries, entry.id) ? document.entries[entry.id] : undefined;
|
|
376
|
-
this.writeBlob(entry, old);
|
|
377
|
-
const row = old ?? { sha256: entry.sha256, bytes: entry.bytes, manifest: clone(entry.manifest), paths: [...entry.files.keys()], claims: [] };
|
|
378
|
-
let claim = this.claim(row, stage.principal);
|
|
379
|
-
if (!claim) {
|
|
380
|
-
claim = { principal: clone(stage.principal), holders: [], uploaded: this.now(), shared: false };
|
|
381
|
-
row.claims.push(claim);
|
|
382
|
-
created.push(entry.id);
|
|
383
|
-
}
|
|
384
|
-
if (!claim.holders.includes(stage.holder))
|
|
385
|
-
claim.holders.push(stage.holder);
|
|
386
|
-
if (claim.holders.length > 64 || row.claims.length > 256)
|
|
387
|
-
throw new StoreError("claim_limit");
|
|
388
|
-
document.entries[entry.id] = row;
|
|
389
|
-
}
|
|
390
|
-
const after = this.save(before, document, this.guardReferences(stage.ids, refs));
|
|
391
|
-
this.stages.delete(stageId);
|
|
392
|
-
return { ok: true, ids: [...stage.ids], created, undo: this.undo(before, after, stage.ids, refs) };
|
|
393
|
-
});
|
|
394
|
-
}
|
|
395
|
-
list(principal) {
|
|
396
|
-
try {
|
|
397
|
-
if (!principalSchema.safeParse(principal).success)
|
|
398
|
-
return fail("invalid_principal");
|
|
399
|
-
return { ok: true, entries: Object.entries(this.load().document.entries).filter(([, r]) => this.visible(r, principal)).map(([id, r]) => this.summary(id, r)) };
|
|
400
|
-
}
|
|
401
|
-
catch (error) {
|
|
402
|
-
return this.error(error);
|
|
403
|
-
}
|
|
404
|
-
}
|
|
405
|
-
managementList(principal) {
|
|
406
|
-
try {
|
|
407
|
-
if (!principalSchema.safeParse(principal).success || principal.kind !== "human")
|
|
408
|
-
return fail("invalid_principal");
|
|
409
|
-
return { ok: true, entries: Object.entries(this.load().document.entries).filter(([, row]) => this.visible(row, principal)).map(([id, row]) => {
|
|
410
|
-
const own = this.claim(row, principal);
|
|
411
|
-
return { ...this.summary(id, row), ownClaim: !!own, sharedByYou: own?.shared === true };
|
|
412
|
-
}) };
|
|
413
|
-
}
|
|
414
|
-
catch (error) {
|
|
415
|
-
return this.error(error);
|
|
416
|
-
}
|
|
417
|
-
}
|
|
418
|
-
read(id, principal) {
|
|
419
|
-
try {
|
|
420
|
-
if (!ID.test(id) || !principalSchema.safeParse(principal).success)
|
|
421
|
-
return fail("not_found");
|
|
422
|
-
const rows = this.load().document.entries;
|
|
423
|
-
const row = Object.hasOwn(rows, id) ? rows[id] : undefined;
|
|
424
|
-
if (!row || !this.visible(row, principal))
|
|
425
|
-
return fail("not_found");
|
|
426
|
-
return { ok: true, entry: this.readRow(id, row) };
|
|
427
|
-
}
|
|
428
|
-
catch {
|
|
429
|
-
return fail("not_found");
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
/** Internal: caller first authorizes its own resource. Knowing an id/holder is never a route capability. */
|
|
433
|
-
readReferenced(id, holder) {
|
|
434
|
-
try {
|
|
435
|
-
if (!ID.test(id) || !this.references().some(r => r.id === id && r.holder === holder))
|
|
436
|
-
return fail("not_found");
|
|
437
|
-
const rows = this.load().document.entries;
|
|
438
|
-
if (!Object.hasOwn(rows, id))
|
|
439
|
-
return fail("not_found");
|
|
440
|
-
return { ok: true, entry: this.readRow(id, rows[id]) };
|
|
441
|
-
}
|
|
442
|
-
catch {
|
|
443
|
-
return fail("not_found");
|
|
444
|
-
}
|
|
445
|
-
}
|
|
446
|
-
setShared(id, principal, shared) {
|
|
447
|
-
return this.transaction(() => {
|
|
448
|
-
if (!ID.test(id) || !principalSchema.safeParse(principal).success || principal.kind !== "human" || typeof shared !== "boolean")
|
|
449
|
-
return fail("not_found");
|
|
450
|
-
const before = this.load(), document = clone(before.document), row = document.entries[id];
|
|
451
|
-
const claim = row && this.claim(row, principal);
|
|
452
|
-
if (!claim)
|
|
453
|
-
return fail("not_found");
|
|
454
|
-
claim.shared = shared;
|
|
455
|
-
this.save(before, document);
|
|
456
|
-
return { ok: true };
|
|
457
|
-
});
|
|
458
|
-
}
|
|
459
|
-
/** Explicit normal delete removes this principal's whole unused claim, including orphan mod holders. */
|
|
460
|
-
deleteClaim(id, principal) {
|
|
461
|
-
return this.transaction(() => {
|
|
462
|
-
this.sweep();
|
|
463
|
-
if (!ID.test(id) || !principalSchema.safeParse(principal).success)
|
|
464
|
-
return fail("not_found");
|
|
465
|
-
const before = this.load(), document = clone(before.document), row = document.entries[id], claim = row && this.claim(row, principal);
|
|
466
|
-
if (!claim)
|
|
467
|
-
return fail("not_found");
|
|
468
|
-
const refs = this.references();
|
|
469
|
-
if (refs.some(r => r.id === id) || [...this.stages.values()].some(s => s.ids.includes(id)))
|
|
470
|
-
return fail("effect_in_use");
|
|
471
|
-
row.claims = row.claims.filter(c => c !== claim);
|
|
472
|
-
this.save(before, document, this.guardReferences([id], refs));
|
|
473
|
-
this.collectLater([id]);
|
|
474
|
-
return { ok: true };
|
|
475
|
-
});
|
|
476
|
-
}
|
|
477
|
-
/** Host-only mod uninstall. Never releases a direct upload or another holder/principal. */
|
|
478
|
-
releaseHolder(holder, principal) {
|
|
479
|
-
return this.transaction(() => {
|
|
480
|
-
if (!MOD.test(holder) || !principalSchema.safeParse(principal).success)
|
|
481
|
-
return fail("invalid_principal");
|
|
482
|
-
const before = this.load(), refs = this.references(), document = clone(before.document), released = [];
|
|
483
|
-
for (const [id, row] of Object.entries(document.entries)) {
|
|
484
|
-
const claim = this.claim(row, principal);
|
|
485
|
-
if (!claim?.holders.includes(holder))
|
|
486
|
-
continue;
|
|
487
|
-
claim.holders = claim.holders.filter(h => h !== holder);
|
|
488
|
-
row.claims = row.claims.filter(c => c.holders.length > 0);
|
|
489
|
-
released.push(id);
|
|
490
|
-
}
|
|
491
|
-
const after = this.save(before, document, this.guardReferences(released, refs));
|
|
492
|
-
this.collectLater(released);
|
|
493
|
-
return { ok: true, released, undo: this.undo(before, after, released, refs) };
|
|
494
|
-
});
|
|
495
|
-
}
|
|
496
|
-
/** Host-only notification after a placement/reference is removed; never a caller capability. */
|
|
497
|
-
collectUnreferenced(ids) {
|
|
498
|
-
if (ids.length > 4096 || ids.some(id => !ID.test(id)))
|
|
499
|
-
throw new Error("Invalid collection ids");
|
|
500
|
-
this.collectLater([...new Set(ids)]);
|
|
501
|
-
}
|
|
502
|
-
collectLater(ids) {
|
|
503
|
-
queueMicrotask(() => {
|
|
504
|
-
this.transaction(() => {
|
|
505
|
-
this.sweep();
|
|
506
|
-
const before = this.load(), refs = this.references(), document = clone(before.document), gone = [];
|
|
507
|
-
let changed = false;
|
|
508
|
-
for (const id of new Set(ids)) {
|
|
509
|
-
const row = document.entries[id];
|
|
510
|
-
if (row?.claims.length || refs.some(r => r.id === id) || [...this.stages.values()].some(s => s.ids.includes(id)))
|
|
511
|
-
continue;
|
|
512
|
-
if (row) {
|
|
513
|
-
delete document.entries[id];
|
|
514
|
-
changed = true;
|
|
515
|
-
}
|
|
516
|
-
gone.push(id);
|
|
517
|
-
}
|
|
518
|
-
if (changed)
|
|
519
|
-
this.save(before, document, this.guardReferences(gone, refs));
|
|
520
|
-
for (const id of gone) {
|
|
521
|
-
try {
|
|
522
|
-
rmSync(join(this.blobs, id), { recursive: true, force: true });
|
|
523
|
-
}
|
|
524
|
-
catch { /* unreferenced debris has no authority */ }
|
|
525
|
-
}
|
|
526
|
-
});
|
|
527
|
-
});
|
|
528
|
-
}
|
|
529
|
-
}
|
|
530
|
-
//# sourceMappingURL=web-effect-library.js.map
|