@embedpdf/plugin-actions 3.0.0-next.14 → 3.0.0-next.16

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 (41) hide show
  1. package/dist/contract/host.cjs +13 -1
  2. package/dist/contract/host.cjs.map +1 -1
  3. package/dist/contract/host.d.cts +3 -7
  4. package/dist/contract/host.d.ts +3 -7
  5. package/dist/contract/host.js +10 -3
  6. package/dist/contract/host.js.map +1 -1
  7. package/dist/contract-BTPW8b4L.d.cts +557 -0
  8. package/dist/contract-Cn-7mBY8.d.ts +557 -0
  9. package/dist/contract-CwN3NAXU.js +175 -0
  10. package/dist/contract-CwN3NAXU.js.map +1 -0
  11. package/dist/contract-_HB_UliT.cjs +210 -0
  12. package/dist/contract-_HB_UliT.cjs.map +1 -0
  13. package/dist/contract.cjs +5 -4
  14. package/dist/contract.d.cts +2 -3
  15. package/dist/contract.d.ts +2 -3
  16. package/dist/contract.js +2 -3
  17. package/dist/host-contract-BbODswDs.d.ts +165 -0
  18. package/dist/host-contract-D22Gz-qt.d.cts +165 -0
  19. package/dist/index.cjs +1588 -1072
  20. package/dist/index.cjs.map +1 -1
  21. package/dist/index.d.cts +11 -9
  22. package/dist/index.d.ts +11 -9
  23. package/dist/index.js +1586 -1072
  24. package/dist/index.js.map +1 -1
  25. package/package.json +7 -11
  26. package/dist/contract-C1K3H4RP.cjs +0 -89
  27. package/dist/contract-C1K3H4RP.cjs.map +0 -1
  28. package/dist/contract-COk-w3Fo.d.ts +0 -50
  29. package/dist/contract-DAem-EST.js +0 -78
  30. package/dist/contract-DAem-EST.js.map +0 -1
  31. package/dist/contract-Os5y4NaD.d.cts +0 -50
  32. package/dist/internal.cjs +0 -3
  33. package/dist/internal.d.cts +0 -3
  34. package/dist/internal.d.ts +0 -3
  35. package/dist/internal.js +0 -2
  36. package/dist/types-BXJLAKXi.cjs +0 -52
  37. package/dist/types-BXJLAKXi.cjs.map +0 -1
  38. package/dist/types-D-DoiESj.d.cts +0 -504
  39. package/dist/types-D-DoiESj.d.ts +0 -504
  40. package/dist/types-DgBMy5nE.js +0 -35
  41. package/dist/types-DgBMy5nE.js.map +0 -1
package/dist/index.cjs CHANGED
@@ -1,68 +1,291 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- const require_types = require("./types-BXJLAKXi.cjs");
3
- const require_contract = require("./contract-C1K3H4RP.cjs");
2
+ const require_contract = require("./contract-_HB_UliT.cjs");
3
+ const require_contract_host = require("./contract/host.cjs");
4
4
  let _embedpdf_core = require("@embedpdf/core");
5
+ let _embedpdf_engine_core_runtime = require("@embedpdf/engine-core/runtime");
5
6
  let _embedpdf_core_acrojs = require("@embedpdf/core-acrojs");
6
- //#region src/script-environment.ts
7
- function createScriptRealmFactory(js, identityDoc) {
8
- const sandboxFactory = js.sandboxFactory ?? (() => import("@embedpdf/core-js-sandbox").then(({ createQuickJsSandbox }) => createQuickJsSandbox()));
9
- const budget = js.budget ?? _embedpdf_core_acrojs.DEFAULT_SCRIPT_BUDGET;
7
+ //#region src/services/annotation-rows.ts
8
+ /**
9
+ * The annotations an action can live on, of both read families: a page's
10
+ * annotations (`doc.annotations`, `doc.annotate.read`) and its widgets, which
11
+ * come with the form (`ActionsForm`, `doc.forms.read`). Each family is read
12
+ * only when the session may read it, so a fill-only session still finds its
13
+ * widgets' actions.
14
+ */
15
+ /** Every annotation of a page the session may read: its annotations, then its widgets. */
16
+ async function readPageAnnotations(ctx, form, page, read = (task) => task) {
17
+ const [annotations, widgets] = await Promise.all([ctx.allows("doc.annotate.read") ? read(ctx.doc.page(page).annotations.list()).then((list) => list.annotations) : Promise.resolve([]), ctx.allows("doc.forms.read") ? form.read(read).then((snapshot) => snapshot.widgets.filter((widget) => widget.page.objectNumber === page.objectNumber)) : Promise.resolve([])]);
18
+ return [...annotations, ...widgets];
19
+ }
20
+ /**
21
+ * The annotation `ref` names, of either family, or `null` when the session
22
+ * can't see it: the form's widgets first (most actions a viewer runs are a
23
+ * field's), then the page's annotations.
24
+ */
25
+ async function readAnnotation(ctx, form, ref, read = (task) => task) {
26
+ const key = (0, _embedpdf_engine_core_runtime.annotationKey)(ref);
27
+ const matches = (candidate) => (0, _embedpdf_engine_core_runtime.annotationKey)(candidate.ref) === key;
28
+ if (ctx.allows("doc.forms.read")) {
29
+ const widget = (await form.read(read)).widgets.find(matches);
30
+ if (widget) return widget;
31
+ }
32
+ if (!ctx.allows("doc.annotate.read")) return null;
33
+ return (await read(ctx.doc.page(ref.page).annotations.list())).annotations.find(matches) ?? null;
34
+ }
35
+ //#endregion
36
+ //#region src/dispatch/fold.ts
37
+ const foldSteps = (steps, diagnostics) => {
38
+ const statuses = steps.map((step) => step.result.status);
10
39
  return {
11
- budget,
12
- realmFor: (target) => (0, _embedpdf_core_acrojs.createScriptHost)({
13
- sandboxFactory,
14
- document: () => {
15
- const meta = target.document();
16
- return {
17
- id: target.doc.id,
18
- fileName: js.fileName?.() ?? meta?.name ?? "document.pdf",
19
- pageCount: meta?.pageCount ?? 0,
20
- pageNumber: 0
21
- };
22
- },
23
- identity: () => (0, _embedpdf_core_acrojs.resolveScriptIdentity)(identityDoc, js.identity),
24
- environment: (sequence) => {
25
- const nowMs = js.now?.() ?? Date.now();
40
+ status: steps.length === 0 ? "inert" : statuses.every((stepStatus) => stepStatus === "refused") ? "refused" : statuses.every((stepStatus) => stepStatus === "inert") ? "inert" : statuses.every((stepStatus) => stepStatus === "executed") ? "executed" : "partial",
41
+ steps,
42
+ diagnostics
43
+ };
44
+ };
45
+ //#endregion
46
+ //#region src/dispatch/dispatcher.ts
47
+ /**
48
+ * The dispatcher: the one serialized queue every tree rides. `execute` runs
49
+ * a resolved tree; `dispatch` resolves a trigger inside the queued operation
50
+ * (submission order is execution order) and fans out through the lifecycle
51
+ * areas. `runAndEmit` is the shared per-tree unit: run, then `onExecuted`.
52
+ *
53
+ * A caller's signal skips an operation that hasn't started, stops a
54
+ * trigger's reads, and stops a tree before its next node. `execute` then
55
+ * rejects `operation-cancelled`; `dispatch` never rejects, so it resolves
56
+ * `cancelled`.
57
+ */
58
+ /** The source a trigger reports for: what its diagnostics carry before a tree is found. */
59
+ const sourceOfTrigger = (trigger) => {
60
+ switch (trigger.scope) {
61
+ case "activate":
62
+ case "annotation": return trigger.source ?? {
63
+ kind: "annotation",
64
+ annotation: trigger.ref,
65
+ page: trigger.page
66
+ };
67
+ case "page": return {
68
+ kind: "page",
69
+ page: trigger.page
70
+ };
71
+ case "document": return { kind: "document" };
72
+ }
73
+ };
74
+ const cancelled = (operation, signal) => new _embedpdf_core.PluginError("operation-cancelled", "actions", `${operation} was cancelled`, { cause: signal?.reason });
75
+ function createDispatcher(ctx, services, { run }, { triggerEnabled, lifecycleAnnotationsOf, planPageSteps }, openSequence, { runDocumentEventOp }) {
76
+ const { executed, reportDiagnostic } = services.events;
77
+ const { decisionFor } = services.policy;
78
+ const { allowsPrint } = services.authority;
79
+ const { enqueue, budget } = services.queue;
80
+ const { readDocumentActions, DOC_EVENT_TREES } = services.catalog;
81
+ const { form } = services;
82
+ const { noteUserActivity } = openSequence;
83
+ /** Run one tree and announce it: the shared per-tree unit (queued by
84
+ * `execute`; called inline per step by the queued trigger resolver). */
85
+ const runAndEmit = async (tree, actionContext, signal) => {
86
+ const result = await run(tree, actionContext, signal);
87
+ executed.emit({
88
+ tree,
89
+ result,
90
+ source: actionContext.source
91
+ });
92
+ return result;
93
+ };
94
+ const execute = (tree, actionContext, options) => {
95
+ const signal = options?.signal;
96
+ if (actionContext.origin === "user") noteUserActivity();
97
+ return enqueue(async () => {
98
+ budget.scriptNodes = 0;
99
+ const result = await runAndEmit(tree, actionContext, signal);
100
+ if (result.status === "cancelled") throw cancelled("actions.execute", signal);
101
+ return result;
102
+ }, options);
103
+ };
104
+ const canExecute = (tree, actionContext) => {
105
+ if (tree.incomplete || !tree.root) return false;
106
+ const decision = decisionFor(tree.root, actionContext.origin);
107
+ return decision === "allow" || decision === "adapter";
108
+ };
109
+ const runSteps = async (steps, origin, event, diagnostics, signal) => {
110
+ const results = [];
111
+ for (const step of steps) {
112
+ if (signal?.aborted) break;
113
+ const result = await runAndEmit(step.tree, {
114
+ origin,
115
+ source: step.source,
116
+ event
117
+ }, signal);
118
+ results.push({
119
+ source: step.source,
120
+ tree: step.tree,
121
+ result
122
+ });
123
+ }
124
+ if (signal?.aborted) return {
125
+ status: "cancelled",
126
+ steps: results,
127
+ diagnostics
128
+ };
129
+ return foldSteps(results, diagnostics);
130
+ };
131
+ /** Everything a trigger needs, reads included, inside the queued operation:
132
+ * submission order is execution order. Never throws. */
133
+ const resolveAndRun = async (trigger, signal) => {
134
+ const diagnostics = [];
135
+ const diagnose = (diagnostic) => {
136
+ diagnostics.push(diagnostic);
137
+ reportDiagnostic(diagnostic, { source: sourceOfTrigger(trigger) });
138
+ };
139
+ /** A read the caller's signal can stop. */
140
+ const read = (task) => ctx.cancellable(signal, task);
141
+ try {
142
+ if (!triggerEnabled(trigger)) {
143
+ diagnose({
144
+ code: "trigger-disabled",
145
+ message: `${trigger.scope}: trigger family disabled by config`
146
+ });
26
147
  return {
27
- nowMs,
28
- utcOffsetMinutes: js.utcOffsetMinutes?.() ?? -new Date(nowMs).getTimezoneOffset(),
29
- randomSeed: js.randomSeed?.() ?? (0, _embedpdf_core_acrojs.seedFrom)(target.doc.id, sequence)
148
+ status: "inert",
149
+ steps: [],
150
+ diagnostics
30
151
  };
31
- },
32
- bootSources: target.bootSources,
33
- budget
34
- })
152
+ }
153
+ const origin = require_contract.triggerOriginOf(trigger);
154
+ switch (trigger.scope) {
155
+ case "activate":
156
+ case "annotation": {
157
+ const event = trigger.scope === "activate" ? "activate" : trigger.event;
158
+ const annotation = await readAnnotation(ctx, form, trigger.ref, read);
159
+ if (event === "mouseUp" && annotation?.actions?.activate) return {
160
+ status: "inert",
161
+ steps: [],
162
+ diagnostics
163
+ };
164
+ const tree = annotation?.actions?.[event];
165
+ if (!tree?.root && !tree?.incomplete) return {
166
+ status: "inert",
167
+ steps: [],
168
+ diagnostics
169
+ };
170
+ const source = sourceOfTrigger(trigger);
171
+ return await runSteps([{
172
+ source,
173
+ tree
174
+ }], origin, require_contract.eventOf(trigger), diagnostics, signal);
175
+ }
176
+ case "page": {
177
+ const lifecycle = await lifecycleAnnotationsOf(trigger.page);
178
+ const pageActions = ctx.getPage(trigger.page)?.actions;
179
+ const steps = planPageSteps(trigger.event, trigger.page, pageActions, lifecycle);
180
+ return await runSteps(steps, origin, require_contract.eventOf(trigger), diagnostics, signal);
181
+ }
182
+ case "document":
183
+ if (trigger.event !== "open") return await runDocumentEventOp(trigger.event, diagnostics, diagnose);
184
+ if (!openSequence.claim()) {
185
+ diagnose({
186
+ code: "open-sequence-replayed",
187
+ message: "document open sequence already fired for this document"
188
+ });
189
+ return {
190
+ status: "inert",
191
+ steps: [],
192
+ diagnostics
193
+ };
194
+ }
195
+ return await openSequence.run();
196
+ }
197
+ } catch (error) {
198
+ if ((0, _embedpdf_core.isPluginError)(error, "operation-cancelled") && signal?.aborted) return {
199
+ status: "cancelled",
200
+ steps: [],
201
+ diagnostics
202
+ };
203
+ diagnose({
204
+ code: "trigger-failed",
205
+ message: `trigger resolution failed: ${error instanceof Error ? error.message : String(error)}`
206
+ });
207
+ return {
208
+ status: "refused",
209
+ steps: [],
210
+ diagnostics
211
+ };
212
+ }
213
+ };
214
+ const dispatch = (trigger, options) => {
215
+ const signal = options?.signal;
216
+ if (require_contract.triggerOriginOf(trigger) === "user") noteUserActivity();
217
+ return enqueue(() => {
218
+ budget.scriptNodes = 0;
219
+ return resolveAndRun(trigger, signal);
220
+ }, options).catch(() => ({
221
+ status: "cancelled",
222
+ steps: [],
223
+ diagnostics: []
224
+ }));
225
+ };
226
+ /** The tree `executeNamed` runs, and its context: a click from your code unless `context` says otherwise. */
227
+ const namedTree = (name) => ({
228
+ root: {
229
+ type: "named",
230
+ subtype: "Named",
231
+ name,
232
+ next: []
233
+ },
234
+ incomplete: false,
235
+ warningFlags: 0,
236
+ warnings: []
237
+ });
238
+ const namedContext = (context) => ({
239
+ origin: "user",
240
+ source: { kind: "api" },
241
+ event: { scope: "activate" },
242
+ ...context
243
+ });
244
+ const executeNamed = (name, context, options) => execute(namedTree(name), namedContext(context), options);
245
+ const canExecuteNamed = (name, context) => canExecute(namedTree(name), namedContext(context)) && (name !== "Print" || allowsPrint());
246
+ /** The raw tree behind a source, read from the document, with no dispatch
247
+ * rules applied (the /A-shadows-U rule lives in `resolveAndRun`). */
248
+ const readActionTree = async (source) => {
249
+ switch (source.kind) {
250
+ case "annotation": return (await readAnnotation(ctx, form, source.annotation))?.actions?.[source.event ?? "activate"] ?? null;
251
+ case "field": {
252
+ const { fields } = await form.read();
253
+ const target = source.field;
254
+ return fields.find((candidate) => target.kind === "objectNumber" ? candidate.ref.kind === "objectNumber" && candidate.ref.objectNumber === target.objectNumber : candidate.name === target.name)?.actions?.[source.event] ?? null;
255
+ }
256
+ case "page": return ctx.getPage(source.page)?.actions?.[source.event ?? "open"] ?? null;
257
+ case "outline": {
258
+ const { items } = await ctx.doc.outline.get();
259
+ return items.find((candidate) => candidate.ref.objectNumber === source.item.objectNumber)?.action ?? null;
260
+ }
261
+ case "document": {
262
+ const snapshot = await readDocumentActions();
263
+ if (!snapshot) return null;
264
+ const event = source.event ?? "open";
265
+ return (event === "open" ? snapshot.openAction : snapshot[DOC_EVENT_TREES[event]]) ?? null;
266
+ }
267
+ }
268
+ };
269
+ return {
270
+ runAndEmit,
271
+ dispatch,
272
+ api: {
273
+ execute,
274
+ canExecute,
275
+ executeNamed,
276
+ canExecuteNamed,
277
+ getActionTree: (source, options) => ctx.cancellable(options?.signal, readActionTree(source)),
278
+ dispatch,
279
+ canDispatch: (trigger) => triggerEnabled(trigger),
280
+ prepareClose: (options) => dispatch({
281
+ scope: "document",
282
+ event: "will-close"
283
+ }, options)
284
+ }
35
285
  };
36
286
  }
37
287
  //#endregion
38
- //#region src/capability.ts
39
- const ALLOW_ALL = {
40
- user: "allow",
41
- hover: "allow",
42
- lifecycle: "allow"
43
- };
44
- const DEFAULT_POLICY = {
45
- goto: ALLOW_ALL,
46
- named: ALLOW_ALL,
47
- hide: ALLOW_ALL,
48
- "reset-form": ALLOW_ALL,
49
- javascript: ALLOW_ALL,
50
- uri: {
51
- user: "adapter",
52
- hover: "report",
53
- lifecycle: "report"
54
- },
55
- print: {
56
- user: "adapter",
57
- hover: "block",
58
- lifecycle: "block"
59
- },
60
- "submit-form": {
61
- user: "adapter",
62
- hover: "block",
63
- lifecycle: "block"
64
- }
65
- };
288
+ //#region src/services/policy.ts
66
289
  /** Never executable, not configurable — diagnostics only. */
67
290
  const NEVER_TYPES = /* @__PURE__ */ new Set([
68
291
  "launch",
@@ -72,7 +295,7 @@ const NEVER_TYPES = /* @__PURE__ */ new Set([
72
295
  "movie",
73
296
  "import-data"
74
297
  ]);
75
- /** Recognized types with no Phase-1 interpreter. */
298
+ /** Recognized types with no interpreter: reported, never executed. */
76
299
  const UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
77
300
  "rendition",
78
301
  "thread",
@@ -83,565 +306,146 @@ const UNSUPPORTED_TYPES = /* @__PURE__ */ new Set([
83
306
  ]);
84
307
  /** Document-lifetime node types: engine mutations, committed in walk order. */
85
308
  const DOCUMENT_TYPES = /* @__PURE__ */ new Set(["reset-form", "javascript"]);
86
- const sameRef = (left, right) => {
87
- if (left.kind === "objectNumber" && right.kind === "objectNumber") return left.annotObjectNumber === right.annotObjectNumber;
88
- if (left.kind === "nm" && right.kind === "nm") return left.pageObjectNumber === right.pageObjectNumber && left.nm === right.nm;
89
- if (left.kind === "index" && right.kind === "index") return left.pageObjectNumber === right.pageObjectNumber && left.index === right.index;
90
- return false;
91
- };
92
309
  const isPrintVerb = (node) => node.type === "named" && node.name === "Print";
93
- function createActionsCapability(ctx, config = {}) {
94
- const policy = {
95
- ...DEFAULT_POLICY,
96
- ...config.policy
97
- };
98
- const enqueue = (0, _embedpdf_core.createSerialQueue)();
99
- const executors = /* @__PURE__ */ new Map();
100
- let annotCommitSink = null;
101
- let formCommitSink = null;
102
- let uiAdapter = null;
103
- /** Sink 1 of the submit chain (consent = installation). */
104
- let submitHandler = null;
105
- /** The form plugin's dataset resolver (D7's one door). */
106
- let submitResolver = null;
107
- /** D3's document-print latch: while a print event wrapper is active, a
108
- * nested print request is SUPPRESSED (`reentrant-print`) — the adapter
109
- * opens exactly one dialog per outer request. */
110
- let docPrintEventActive = false;
111
- /** D11's deterministic aggregate: JS nodes run in the CURRENT dispatch. */
112
- let jsNodesThisDispatch = 0;
113
- const actionHook = (0, _embedpdf_core.createEventHook)((error) => globalThis.console?.error("[actions] onAction observer failed:", error));
114
- const diagnosticHook = (0, _embedpdf_core.createEventHook)((error) => globalThis.console?.error("[actions] onDiagnostic observer failed:", error));
115
- const scriptDiagnosticHook = (0, _embedpdf_core.createEventHook)((error) => globalThis.console?.error("[actions] onScriptDiagnostic observer failed:", error));
116
- const scriptErrorHook = (0, _embedpdf_core.createEventHook)((error) => globalThis.console?.error("[actions] onScriptError observer failed:", error));
117
- ctx.cleanup(() => {
118
- actionHook.dispose();
119
- diagnosticHook.dispose();
120
- scriptDiagnosticHook.dispose();
121
- scriptErrorHook.dispose();
122
- });
123
- /** Policy lookup — `null` means "recognized, no interpreter" (inert). */
310
+ function createPolicy(ctx) {
311
+ const settings = ctx.settings();
312
+ /** The live policy: read at decision time, never captured. */
313
+ const current = () => settings.get().policy;
314
+ /** The decision for one node and origin; `'never'` and `'unsupported'` are fixed, not configurable. */
124
315
  const decisionFor = (node, origin) => {
125
316
  if (NEVER_TYPES.has(node.type)) return "never";
126
317
  if (UNSUPPORTED_TYPES.has(node.type)) return "unsupported";
318
+ const policy = current();
127
319
  if (isPrintVerb(node)) return policy.print[origin];
128
320
  const row = policy[node.type];
129
321
  return row ? row[origin] : "unsupported";
130
322
  };
131
- const allowsPrint = () => ctx.tryGet(_embedpdf_core.DocumentsToken)?.allows("doc.print", ctx.documentId ?? void 0) ?? true;
132
- let actionsSnapshotPromise = null;
133
- const readDocumentActions = () => {
134
- if (!actionsSnapshotPromise) {
135
- const doc = ctx.doc;
136
- const memo = (doc?.actions ? Promise.resolve(doc.actions.read()) : Promise.resolve(null)).catch((error) => {
137
- if (actionsSnapshotPromise === memo) actionsSnapshotPromise = null;
138
- throw error;
139
- });
140
- actionsSnapshotPromise = memo;
141
- }
142
- return actionsSnapshotPromise;
323
+ return {
324
+ decisionFor,
325
+ current
143
326
  };
144
- /** The Table-200 key for each verb-shaped document trigger event. */
145
- const DOC_EVENT_TREES = {
146
- "will-save": "willSave",
147
- "did-save": "didSave",
148
- "will-print": "willPrint",
149
- "did-print": "didPrint",
150
- "will-close": "willClose"
327
+ }
328
+ //#endregion
329
+ //#region src/submit/intent.ts
330
+ const intentOfPayload = (payload) => ({
331
+ url: payload.url,
332
+ fields: payload.fields,
333
+ exclude: payload.flags.exclude,
334
+ includeNoValueFields: payload.flags.includeNoValueFields,
335
+ format: payload.flags.format,
336
+ method: payload.flags.method,
337
+ flagsRaw: payload.flags.raw,
338
+ ...payload.charSet === void 0 ? {} : { charSet: payload.charSet }
339
+ });
340
+ const toFormSubmissionRequest = (request, sentAt) => ({
341
+ entries: request.entries,
342
+ intent: {
343
+ url: request.url,
344
+ format: request.format,
345
+ method: request.method,
346
+ flagsRaw: request.flagsRaw,
347
+ ...request.charSet === void 0 ? {} : { charSet: request.charSet }
348
+ },
349
+ origin: request.origin,
350
+ sentAt
351
+ });
352
+ /** Script `doc.submitForm(...)` → the one normalized intent. Script field
353
+ * names are include-mode by definition (Acrobat's aFields). */
354
+ const intentOfSubmitEffect = (effect) => {
355
+ const format = effect.format ?? "fdf";
356
+ return {
357
+ url: effect.url,
358
+ fields: effect.fieldNames?.map((name) => ({
359
+ kind: "name",
360
+ name
361
+ })) ?? null,
362
+ exclude: false,
363
+ includeNoValueFields: effect.includeEmpty,
364
+ format,
365
+ method: effect.method ?? "post",
366
+ flagsRaw: 0
151
367
  };
152
- const nowMs = () => js?.now?.() ?? Date.now();
153
- const intentOfPayload = (payload) => ({
154
- url: payload.url,
155
- fields: payload.fields,
156
- exclude: payload.flags.exclude,
157
- includeNoValueFields: payload.flags.includeNoValueFields,
158
- format: payload.flags.format,
159
- method: payload.flags.method,
160
- flagsRaw: payload.flags.raw,
161
- ...payload.charSet === void 0 ? {} : { charSet: payload.charSet }
162
- });
163
- const toFormSubmissionRequest = (request) => ({
164
- entries: request.entries,
165
- intent: {
166
- url: request.url,
167
- format: request.format,
168
- method: request.method,
169
- flagsRaw: request.flagsRaw,
170
- ...request.charSet === void 0 ? {} : { charSet: request.charSet }
171
- },
172
- origin: request.origin,
173
- clientTimeMs: nowMs()
174
- });
175
- /**
176
- * The ONE submit door — both sources land here after policy. Sink order:
177
- * embedder handler (explicit beats ambient; detached-promise contract) →
178
- * the document's home (`doc.forms.submit`, engine-asserted, AWAITED — a
179
- * real op with a real receipt) → blocked with `no-submit-sink`. Never a
180
- * network call from this plugin.
181
- */
182
- const performSubmit = async (intent, actionCtx, diagnose) => {
183
- const decision = policy["submit-form"][actionCtx.origin];
184
- if (decision !== "adapter" && decision !== "allow") {
185
- diagnose({
186
- code: "blocked",
187
- message: `submit-form: policy '${decision}' for origin '${actionCtx.origin}'`
188
- });
189
- return { status: "blocked" };
190
- }
191
- if (!submitResolver) {
192
- diagnose({
193
- code: "no-submit-resolver",
194
- message: "submit-form: no dataset resolver registered (form plugin missing?)"
368
+ };
369
+ //#endregion
370
+ //#region src/dispatch/run.ts
371
+ function createRunner(ctx, services, { performSubmit }, { firePrintThroughAdapter }) {
372
+ const { decisionFor } = services.policy;
373
+ const ports = services.ports.slots;
374
+ const { reportDiagnostic } = services.events;
375
+ const { form } = services;
376
+ async function run(tree, actionContext, signal) {
377
+ const diagnostics = [];
378
+ /** Record a diagnostic in the result and report it, with the action it concerns. */
379
+ const diagnose = (diagnostic, action) => {
380
+ diagnostics.push(diagnostic);
381
+ reportDiagnostic(diagnostic, {
382
+ action,
383
+ source: actionContext.source
195
384
  });
196
- return {
197
- status: "blocked",
198
- detail: "no dataset resolver"
199
- };
200
- }
201
- let request;
202
- try {
203
- request = await submitResolver(intent, actionCtx, diagnose);
204
- } catch (error) {
205
- const detail = error instanceof Error ? error.message : String(error);
385
+ };
386
+ if (tree.incomplete) {
206
387
  diagnose({
207
- code: "executor-failed",
208
- message: `submit-form: dataset resolution: ${detail}`
209
- });
388
+ code: "incomplete-tree",
389
+ message: "refused: the action tree is incomplete"
390
+ }, tree.root?.type ?? null);
210
391
  return {
211
- status: "failed",
212
- detail
392
+ status: "refused",
393
+ nodes: [],
394
+ diagnostics
213
395
  };
214
396
  }
215
- const doc = ctx.doc;
216
- const homeSubmit = doc && typeof doc.forms.submit === "function" ? () => Promise.resolve(doc.forms.submit(toFormSubmissionRequest(request))) : null;
217
- if (submitHandler) try {
218
- const outcome = submitHandler(request, { submitToDocumentHome: homeSubmit });
219
- if (outcome && typeof outcome.then === "function") outcome.catch((error) => {
220
- diagnosticHook.emit({
397
+ if (!tree.root) return {
398
+ status: "inert",
399
+ nodes: [],
400
+ diagnostics
401
+ };
402
+ const nodes = [];
403
+ const deferred = [];
404
+ let documentFailed = false;
405
+ let cancelled = false;
406
+ const settle = (result, outcome) => {
407
+ if (outcome.status === "executed") result.status = "executed";
408
+ else if (outcome.status === "inert") {
409
+ result.status = "inert";
410
+ result.detail = outcome.reason;
411
+ diagnose({
412
+ code: "executor-inert",
413
+ message: `${result.type}: ${outcome.reason}`
414
+ }, result.type);
415
+ } else {
416
+ result.status = "failed";
417
+ result.detail = outcome.error;
418
+ diagnose({
221
419
  code: "executor-failed",
222
- message: `submit handler rejected (detached): ${error instanceof Error ? error.message : String(error)}`
223
- });
224
- });
225
- return { status: "executed" };
226
- } catch (error) {
227
- const detail = error instanceof Error ? error.message : String(error);
228
- diagnose({
229
- code: "executor-failed",
230
- message: `submit handler threw: ${detail}`
231
- });
232
- return {
233
- status: "failed",
234
- detail
235
- };
236
- }
237
- if (homeSubmit) try {
238
- await homeSubmit();
239
- return { status: "executed" };
240
- } catch (error) {
241
- const detail = error instanceof Error ? error.message : String(error);
242
- diagnose({
243
- code: "executor-failed",
244
- message: `document home refused submit: ${detail}`
245
- });
246
- return {
247
- status: "failed",
248
- detail
249
- };
250
- }
251
- diagnose({
252
- code: "no-submit-sink",
253
- message: "submit-form: no handler installed and the document has no submit-capable home"
254
- });
255
- return {
256
- status: "blocked",
257
- detail: "no submit sink"
258
- };
259
- };
260
- const js = config.javascript;
261
- const realms = js?.enabled && ctx.doc ? createScriptRealmFactory(js, ctx.doc) : null;
262
- const scriptHost = realms && ctx.doc ? realms.realmFor({
263
- doc: ctx.doc,
264
- document: () => ctx.document(),
265
- bootSources: async () => {
266
- return ((await readDocumentActions())?.nameTreeScripts ?? []).map(({ action }) => (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(action));
267
- }
268
- }) : null;
269
- if (scriptHost) ctx.cleanup(() => scriptHost.dispose());
270
- const surfaceScriptResult = (result) => {
271
- const uiContext = {
272
- origin: result.origin,
273
- phase: result.phase
274
- };
275
- for (const effect of result.uiEffects) {
276
- if (result.realm === "detached" && effect.kind !== "alert") {
277
- scriptDiagnosticHook.emit({
278
- code: "ui-effect-suppressed",
279
- message: `script ${effect.kind} request from a detached realm suppressed: no document surface`
280
- });
281
- continue;
282
- }
283
- if (effect.kind === "submitForm") {
284
- performSubmit(intentOfSubmitEffect(effect), {
285
- origin: result.origin,
286
- source: { kind: "api" },
287
- event: { scope: "activate" }
288
- }, (diagnostic) => diagnosticHook.emit(diagnostic));
289
- continue;
290
- }
291
- if (effect.kind === "print" && !allowsPrint()) {
292
- scriptDiagnosticHook.emit({
293
- code: "ui-effect-suppressed",
294
- message: "script print request withheld: doc.print is not allowed"
295
- });
296
- continue;
297
- }
298
- if (effect.kind === "print" && docPrintEventActive) {
299
- scriptDiagnosticHook.emit({
300
- code: "ui-effect-suppressed",
301
- message: "script print request during a document print event — suppressed (reentrant)"
302
- });
303
- continue;
304
- }
305
- if (!uiAdapter) {
306
- diagnosticHook.emit({
307
- code: "no-adapter",
308
- message: `script ${effect.kind}: no UI adapter installed`
309
- });
310
- continue;
311
- }
312
- if (effect.kind === "alert") uiAdapter.alert?.(effect.message, {
313
- ...uiContext,
314
- icon: effect.icon,
315
- ...effect.title !== void 0 ? { title: effect.title } : {}
316
- });
317
- else if (effect.kind === "gotoPage") uiAdapter.gotoPage?.(effect.page, uiContext);
318
- else uiAdapter.print(uiContext);
319
- }
320
- for (const diagnostic of result.diagnostics) scriptDiagnosticHook.emit(diagnostic);
321
- if (result.error) scriptErrorHook.emit(result.error);
322
- };
323
- /** A K/V/C/F commit surfaces as up to two results: the boot phase (only
324
- * when it produced effects) and the user phase (always — it carries the
325
- * diagnostics and the error). */
326
- const surfaceScriptCommit = (commit, context) => {
327
- for (const phase of ["boot", "user"]) {
328
- const uiEffects = commit.uiEffects.filter((effect) => effect.phase === phase);
329
- if (uiEffects.length === 0 && phase === "boot") continue;
330
- surfaceScriptResult({
331
- uiEffects,
332
- diagnostics: phase === "user" ? commit.diagnostics : [],
333
- ...phase === "user" && commit.error ? { error: commit.error } : {},
334
- origin: context.origin,
335
- phase,
336
- realm: context.realm
337
- });
338
- }
339
- };
340
- /** Script `doc.submitForm(...)` → the one normalized intent. Script field
341
- * names are include-mode by definition (Acrobat's aFields). */
342
- const intentOfSubmitEffect = (effect) => {
343
- const format = effect.format ?? "fdf";
344
- return {
345
- url: effect.url,
346
- fields: effect.fieldNames?.map((name) => ({
347
- kind: "name",
348
- name
349
- })) ?? null,
350
- exclude: false,
351
- includeNoValueFields: effect.includeEmpty,
352
- format,
353
- method: effect.method ?? "post",
354
- flagsRaw: 0
355
- };
356
- };
357
- const engineColorToArray = (color) => color ? [
358
- "RGB",
359
- color.r / 255,
360
- color.g / 255,
361
- color.b / 255
362
- ] : ["T"];
363
- const pageIndexOf = (pon) => ctx.document()?.pages.findIndex((page) => page.pageObjectNumber === pon) ?? -1;
364
- const scriptWorldFor = async (pon) => {
365
- const doc = ctx.doc;
366
- const snapshot = await doc.forms.list();
367
- const pageIndex = Math.max(0, pageIndexOf(pon));
368
- const { annotations } = await doc.page(pon).annotations.list();
369
- const annots = annotations.filter((a) => a.subtype !== "link" && !a.subtype.startsWith("widget")).map((a) => {
370
- const styled = a;
371
- return {
372
- ref: a.ref,
373
- name: a.nm ?? "",
374
- subtype: a.subtype,
375
- page: pageIndex,
376
- rect: [
377
- a.rect.left,
378
- a.rect.bottom,
379
- a.rect.right,
380
- a.rect.top
381
- ],
382
- contents: a.contents ?? "",
383
- author: a.author ?? "",
384
- subject: a.subject ?? "",
385
- strokeColor: engineColorToArray(styled.color),
386
- fillColor: engineColorToArray(styled.interiorColor),
387
- opacity: styled.opacity ?? 1,
388
- width: styled.strokeWidth ?? 1,
389
- borderStyle: styled.borderStyle === "dashed" ? "D" : "S",
390
- dash: styled.dashArray ?? [],
391
- hidden: a.flags.hidden,
392
- print: a.flags.print,
393
- readOnly: a.flags.readOnly,
394
- locked: a.flags.locked,
395
- noView: a.flags.noView,
396
- toggleNoView: a.flags.toggleNoView,
397
- opaqueBody: a.subtype === "stamp"
398
- };
399
- });
400
- return {
401
- snapshot,
402
- world: {
403
- fields: (0, _embedpdf_core_acrojs.scriptFieldsFromSnapshot)(snapshot),
404
- annots,
405
- annotPages: [pageIndex],
406
- annotsCoverDocument: (ctx.document()?.pageCount ?? 0) <= 1
407
- }
408
- };
409
- };
410
- const ANNOTATION_EVENT_NAMES = {
411
- cursorEnter: "Mouse Enter",
412
- cursorExit: "Mouse Exit",
413
- mouseDown: "Mouse Down",
414
- mouseUp: "Mouse Up",
415
- focus: "Focus",
416
- blur: "Blur"
417
- };
418
- const PAGE_EVENT_NAMES = {
419
- open: "Open",
420
- close: "Close",
421
- visible: "Visible",
422
- invisible: "Invisible"
423
- };
424
- const DOC_EVENT_NAMES = {
425
- open: "Open",
426
- "will-save": "WillSave",
427
- "did-save": "DidSave",
428
- "will-print": "WillPrint",
429
- "did-print": "DidPrint",
430
- "will-close": "WillClose"
431
- };
432
- const scriptEventFor = (actionCtx, snapshot) => {
433
- const provenance = actionCtx.event;
434
- const type = provenance.scope === "page" ? "Page" : provenance.scope === "document" ? "Doc" : actionCtx.source.kind === "widget" ? "Field" : actionCtx.source.kind === "link" ? "Link" : "Annot";
435
- const name = provenance.scope === "activate" ? "Mouse Up" : provenance.scope === "annotation" ? ANNOTATION_EVENT_NAMES[provenance.name] ?? "Mouse Up" : provenance.scope === "page" ? PAGE_EVENT_NAMES[provenance.name] ?? "Open" : DOC_EVENT_NAMES[provenance.name] ?? "Open";
436
- const targetRef = actionCtx.source.kind === "widget" ? actionCtx.source.field : void 0;
437
- const targetField = targetRef ? snapshot.fields.find((field) => targetRef.kind === "objectNumber" && field.ref.kind === "objectNumber" ? field.ref.fieldObjectNumber === targetRef.fieldObjectNumber : targetRef.kind === "fqn" && field.name === targetRef.name) : void 0;
438
- return {
439
- kind: "widget-activate",
440
- type,
441
- name,
442
- ...targetRef ? {
443
- target: targetRef,
444
- source: targetRef
445
- } : {},
446
- ...targetField && targetField.valueEntry.kind === "scalar" ? { value: targetField.valueEntry.value } : {}
447
- };
448
- };
449
- /** Commit one run's document effects through the owner sinks in the
450
- * DECLARED order (form first, then annot); the first failure skips the
451
- * rest across BOTH streams. Returns the failure summary, if any. */
452
- const commitScriptOutput = async (output, diagnose) => {
453
- let failure = null;
454
- if (output.formEffects.length > 0) if (!formCommitSink) diagnose({
455
- code: "no-commit-sink",
456
- message: `script form effects dropped: no form commit sink registered (${output.formEffects.length})`
457
- });
458
- else {
459
- const bad = (await formCommitSink(output.formEffects)).results.find((entry) => entry.status === "failed" || entry.status === "rejected");
460
- if (bad) failure = `form effect ${bad.index}: ${bad.error?.message ?? bad.status}`;
461
- }
462
- if (output.annotEffects.length > 0) if (failure) diagnose({
463
- code: "executor-failed",
464
- message: `script annot effects skipped after form failure (${output.annotEffects.length})`
465
- });
466
- else if (!annotCommitSink) diagnose({
467
- code: "no-commit-sink",
468
- message: `script annot effects dropped: no annotation commit sink registered (${output.annotEffects.length})`
469
- });
470
- else {
471
- const entries = output.annotEffects.map((effect) => ({
472
- annotObjectNumber: effect.ref.kind === "objectNumber" ? effect.ref.annotObjectNumber : -1,
473
- ...effect.ref.kind === "objectNumber" ? { pageObjectNumber: effect.ref.pageObjectNumber } : {},
474
- patch: effect.patch
475
- }));
476
- const bad = (await annotCommitSink(entries)).results.find((entry) => entry.status === "failed");
477
- if (bad) failure = `annotation ${bad.annotObjectNumber}: ${bad.error ?? "failed"}`;
478
- }
479
- if (failure) diagnose({
480
- code: "executor-failed",
481
- message: `script commit: ${failure}`
482
- });
483
- return failure;
484
- };
485
- if (scriptHost) {
486
- const nodeCap = js?.maxScriptNodesPerDispatch ?? 16;
487
- executors.set("javascript", async (node, actionCtx) => {
488
- if (node.type !== "javascript") return {
489
- status: "inert",
490
- reason: "not a JS node"
491
- };
492
- if (!ctx.doc) return {
493
- status: "inert",
494
- reason: "no document"
495
- };
496
- if (jsNodesThisDispatch >= nodeCap) return {
497
- status: "inert",
498
- reason: `dispatch script budget exhausted (${nodeCap} JS nodes)`
499
- };
500
- jsNodesThisDispatch += 1;
501
- const source = actionCtx.source;
502
- const pon = source.kind === "widget" ? source.pon : source.kind === "link" || source.kind === "annotation" ? source.pon ?? ctx.document()?.pages[0]?.pageObjectNumber : source.kind === "page" ? source.pon : ctx.document()?.pages[0]?.pageObjectNumber;
503
- if (pon === void 0) return {
504
- status: "inert",
505
- reason: "no page to anchor the world on"
506
- };
507
- const diagnose = (diagnostic) => diagnosticHook.emit(diagnostic);
508
- const pendingExternal = [];
509
- const splitExternal = (output, phase) => output.uiEffects.filter((effect) => {
510
- if (effect.kind === "print" || effect.kind === "submitForm") {
511
- pendingExternal.push({
512
- effect,
513
- phase
514
- });
515
- return false;
516
- }
517
- return true;
518
- });
519
- const outcome = await scriptHost.transaction(async (txn) => {
520
- let built = await scriptWorldFor(pon);
521
- const boot = await txn.boot({
522
- ...built.world,
523
- event: {
524
- kind: "name-tree-boot",
525
- type: "Doc",
526
- name: "Open"
527
- }
528
- });
529
- if (boot) {
530
- await commitScriptOutput(boot, diagnose);
531
- surfaceScriptResult({
532
- uiEffects: splitExternal(boot, "boot"),
533
- diagnostics: boot.diagnostics,
534
- ...boot.error ? { error: boot.error } : {},
535
- origin: actionCtx.origin,
536
- phase: "boot",
537
- realm: "document"
538
- });
539
- if (boot.formEffects.length || boot.annotEffects.length) built = await scriptWorldFor(pon);
540
- }
541
- const output = await txn.run(node.script, {
542
- ...built.world,
543
- event: scriptEventFor(actionCtx, built.snapshot)
544
- });
545
- surfaceScriptResult({
546
- uiEffects: splitExternal(output, "user"),
547
- diagnostics: output.diagnostics,
548
- ...output.error ? { error: output.error } : {},
549
- origin: actionCtx.origin,
550
- phase: "user",
551
- realm: "document"
552
- });
553
- if (output.error) return {
554
- status: "failed",
555
- error: output.error.message
556
- };
557
- const failure = await commitScriptOutput(output, diagnose);
558
- return failure ? {
559
- status: "failed",
560
- error: failure
561
- } : { status: "executed" };
562
- });
563
- for (const entry of pendingExternal) if (entry.effect.kind === "print") await firePrintThroughAdapter({
564
- origin: actionCtx.origin,
565
- phase: entry.phase
566
- }, diagnose);
567
- else await performSubmit(intentOfSubmitEffect(entry.effect), actionCtx, diagnose);
568
- return outcome;
569
- });
570
- }
571
- async function run(tree, actionCtx) {
572
- const diagnostics = [];
573
- const diagnose = (diagnostic) => {
574
- diagnostics.push(diagnostic);
575
- diagnosticHook.emit(diagnostic);
576
- };
577
- if (tree.incomplete) {
578
- diagnose({
579
- code: "incomplete-tree",
580
- message: "refused: the action tree is incomplete"
581
- });
582
- return {
583
- status: "refused",
584
- nodes: [],
585
- diagnostics
586
- };
587
- }
588
- if (!tree.root) return {
589
- status: "inert",
590
- nodes: [],
591
- diagnostics
592
- };
593
- const nodes = [];
594
- const deferred = [];
595
- let documentFailed = false;
596
- const settle = (result, outcome) => {
597
- if (outcome.status === "executed") result.status = "executed";
598
- else if (outcome.status === "inert") {
599
- result.status = "inert";
600
- result.detail = outcome.reason;
601
- diagnose({
602
- code: "executor-inert",
603
- message: `${result.type}: ${outcome.reason}`
604
- });
605
- } else {
606
- result.status = "failed";
607
- result.detail = outcome.error;
608
- diagnose({
609
- code: "executor-failed",
610
- message: `${result.type}: ${outcome.error}`
611
- });
612
- }
420
+ message: `${result.type}: ${outcome.error}`
421
+ }, result.type);
422
+ }
613
423
  };
614
424
  const interpretHide = async (node) => {
615
- const doc = ctx.doc;
616
425
  const display = node.hide ? "hidden" : "visible";
426
+ const diagnoseHide = (diagnostic) => diagnose(diagnostic, "hide");
617
427
  const fieldRefs = [];
618
428
  const annotEntries = [];
619
429
  const names = [];
620
430
  const bareObjectNumbers = [];
621
431
  for (const target of node.targets) if (target.kind === "objectNumber") bareObjectNumbers.push(target.objectNumber);
622
432
  else names.push(target.name);
623
- if ((names.length || bareObjectNumbers.length) && doc) {
624
- const snapshot = names.length || bareObjectNumbers.length ? await doc.forms.list() : null;
433
+ if (names.length || bareObjectNumbers.length) {
434
+ const snapshot = await form.read();
625
435
  for (const name of names) {
626
- const field = snapshot?.fields.find((candidate) => candidate.name === name);
436
+ const field = snapshot.fields.find((candidate) => candidate.name === name);
627
437
  if (!field) {
628
- diagnose({
438
+ diagnoseHide({
629
439
  code: "unresolved-target",
630
440
  message: `hide: no field named '${name}'`
631
441
  });
632
442
  continue;
633
443
  }
634
- fieldRefs.push({
635
- kind: "objectNumber",
636
- fieldObjectNumber: field.fieldObjectNumber
637
- });
444
+ fieldRefs.push(field.ref);
638
445
  }
639
446
  for (const objectNumber of bareObjectNumbers) {
640
- const owner = snapshot?.fields.find((field) => field.widgets.some((widget) => widget.annotObjectNumber === objectNumber));
641
- if (owner) fieldRefs.push({
642
- kind: "objectNumber",
643
- fieldObjectNumber: owner.fieldObjectNumber
644
- });
447
+ const owner = snapshot.fields.find((field) => field.widgets.some((widget) => widget.objectNumber === objectNumber));
448
+ if (owner) fieldRefs.push(owner.ref);
645
449
  else annotEntries.push({
646
450
  annotObjectNumber: objectNumber,
647
451
  patch: { flags: { hidden: node.hide } }
@@ -650,37 +454,37 @@ function createActionsCapability(ctx, config = {}) {
650
454
  }
651
455
  let failedDetail;
652
456
  if (fieldRefs.length) {
653
- if (!formCommitSink) {
654
- diagnose({
457
+ if (!ports.formCommitSink) {
458
+ diagnoseHide({
655
459
  code: "no-commit-sink",
656
460
  message: "hide: no form commit sink registered (form plugin absent)"
657
461
  });
658
462
  return { status: "no-executor" };
659
463
  }
660
- const bad = (await formCommitSink(fieldRefs.map((ref) => ({
464
+ const result = await ports.formCommitSink(fieldRefs.map((ref) => ({
661
465
  kind: "setDisplay",
662
466
  ref,
663
467
  display
664
- })))).results.find((entry) => entry.status === "failed" || entry.status === "rejected");
665
- if (bad) {
666
- failedDetail = bad.error?.message ?? bad.status;
667
- diagnose({
468
+ })));
469
+ if (result.status === "failed") {
470
+ failedDetail = result.error ?? "failed";
471
+ diagnoseHide({
668
472
  code: "executor-failed",
669
473
  message: `hide: ${failedDetail}`
670
474
  });
671
475
  }
672
476
  }
673
477
  if (annotEntries.length && !failedDetail) {
674
- if (!annotCommitSink) {
675
- diagnose({
478
+ if (!ports.annotCommitSink) {
479
+ diagnoseHide({
676
480
  code: "no-commit-sink",
677
481
  message: "hide: no annotation commit sink registered (annotation plugin absent)"
678
482
  });
679
483
  return { status: "no-executor" };
680
484
  }
681
- const committed = await annotCommitSink(annotEntries);
485
+ const committed = await ports.annotCommitSink(annotEntries);
682
486
  const failed = committed.results.filter((entry) => entry.status === "failed");
683
- for (const failure of failed) diagnose({
487
+ for (const failure of failed) diagnoseHide({
684
488
  code: "executor-failed",
685
489
  message: `hide: annotation ${failure.annotObjectNumber}: ${failure.error ?? "failed"}`
686
490
  });
@@ -699,12 +503,13 @@ function createActionsCapability(ctx, config = {}) {
699
503
  status: "blocked"
700
504
  };
701
505
  nodes.push(result);
702
- const decision = decisionFor(node, actionCtx.origin);
506
+ const decision = decisionFor(node, actionContext.origin);
507
+ const diagnoseNode = (diagnostic) => diagnose(diagnostic, node.type);
703
508
  if (decision === "never" || decision === "block" || decision === "report") {
704
509
  result.status = "blocked";
705
- diagnose({
510
+ if (decision !== "block") diagnoseNode({
706
511
  code: "blocked",
707
- message: `${node.type}: ${decision === "never" ? "never executable" : `policy '${decision}' for origin '${actionCtx.origin}'`}`
512
+ message: `${node.type}: ${decision === "never" ? "never executable" : `policy 'report' for origin '${actionContext.origin}'`}`
708
513
  });
709
514
  return;
710
515
  }
@@ -728,17 +533,17 @@ function createActionsCapability(ctx, config = {}) {
728
533
  result.status = "skipped";
729
534
  return;
730
535
  }
731
- const executor = executors.get(node.type);
536
+ const executor = ports.executors.get(node.type);
732
537
  if (!executor) {
733
538
  result.status = node.type === "javascript" ? "inert" : "no-executor";
734
- diagnose({
539
+ diagnoseNode({
735
540
  code: "no-executor",
736
541
  message: `${node.type}: no executor registered${node.type === "javascript" ? " (scripting unavailable)" : ""}`
737
542
  });
738
543
  return;
739
544
  }
740
545
  try {
741
- settle(result, await executor(node, actionCtx));
546
+ settle(result, await executor(node, actionContext));
742
547
  } catch (error) {
743
548
  settle(result, {
744
549
  status: "failed",
@@ -752,7 +557,7 @@ function createActionsCapability(ctx, config = {}) {
752
557
  if (!node.payload) {
753
558
  result.status = "inert";
754
559
  result.detail = "submit payload unavailable (older runtime extraction)";
755
- diagnose({
560
+ diagnoseNode({
756
561
  code: "submit-payload-unavailable",
757
562
  message: "submit-form: payload unavailable (older runtime extraction) — node inert"
758
563
  });
@@ -763,7 +568,7 @@ function createActionsCapability(ctx, config = {}) {
763
568
  deferred.push({
764
569
  result,
765
570
  fire: async () => {
766
- const outcome = await performSubmit(intentOfPayload(payload), actionCtx, diagnose);
571
+ const outcome = await performSubmit(intentOfPayload(payload), actionContext, diagnoseNode);
767
572
  result.status = outcome.status;
768
573
  if (outcome.detail) result.detail = outcome.detail;
769
574
  }
@@ -776,22 +581,22 @@ function createActionsCapability(ctx, config = {}) {
776
581
  result,
777
582
  fire: async () => {
778
583
  if (node.type === "uri") {
779
- if (!uiAdapter) {
584
+ if (!ports.uiAdapter) {
780
585
  result.status = "no-executor";
781
- diagnose({
586
+ diagnoseNode({
782
587
  code: "no-adapter",
783
588
  message: `${node.type}: no UI adapter installed`
784
589
  });
785
590
  return;
786
591
  }
787
- uiAdapter.openUri(node.uri, {
592
+ ports.uiAdapter.openUri(node.uri, {
788
593
  isMap: node.isMap,
789
- origin: actionCtx.origin
594
+ origin: actionContext.origin
790
595
  });
791
596
  result.status = "executed";
792
597
  return;
793
598
  }
794
- const outcome = await firePrintThroughAdapter(void 0, diagnose);
599
+ const outcome = await firePrintThroughAdapter(void 0, diagnoseNode);
795
600
  result.status = outcome.status;
796
601
  if (outcome.detail) result.detail = outcome.detail;
797
602
  }
@@ -802,17 +607,17 @@ function createActionsCapability(ctx, config = {}) {
802
607
  deferred.push({
803
608
  result,
804
609
  fire: async () => {
805
- const executor = executors.get(node.type);
610
+ const executor = ports.executors.get(node.type);
806
611
  if (!executor) {
807
612
  result.status = "no-executor";
808
- diagnose({
613
+ diagnoseNode({
809
614
  code: "no-executor",
810
615
  message: `${node.type}: no executor registered`
811
616
  });
812
617
  return;
813
618
  }
814
619
  try {
815
- settle(result, await executor(node, actionCtx));
620
+ settle(result, await executor(node, actionContext));
816
621
  } catch (error) {
817
622
  settle(result, {
818
623
  status: "failed",
@@ -823,11 +628,20 @@ function createActionsCapability(ctx, config = {}) {
823
628
  });
824
629
  };
825
630
  const walk = async (node, path) => {
631
+ if (signal?.aborted) {
632
+ cancelled = true;
633
+ return;
634
+ }
826
635
  await interpret(node, path);
827
- for (let index = 0; index < node.next.length; index++) await walk(node.next[index], [...path, index]);
636
+ for (let index = 0; index < node.next.length && !cancelled; index++) await walk(node.next[index], [...path, index]);
828
637
  };
829
638
  await walk(tree.root, []);
830
- if (!documentFailed) for (const entry of deferred) await entry.fire();
639
+ if (!documentFailed && !cancelled) for (const entry of deferred) await entry.fire();
640
+ if (cancelled) return {
641
+ status: "cancelled",
642
+ nodes,
643
+ diagnostics
644
+ };
831
645
  const anyExecuted = nodes.some((node) => node.status === "executed");
832
646
  const anyFailedOrSkipped = nodes.some((node) => node.status === "failed" || node.status === "skipped");
833
647
  return {
@@ -836,239 +650,426 @@ function createActionsCapability(ctx, config = {}) {
836
650
  diagnostics
837
651
  };
838
652
  }
839
- /** run + seq + event — the shared per-tree unit (queued by execute; called
840
- * inline per step by the queued trigger resolver). */
841
- const runAndEmit = async (tree, actionCtx) => {
842
- const result = await run(tree, actionCtx);
843
- ctx.dispatch({ type: "ACTIONS_DISPATCHED" });
844
- actionHook.emit({
845
- ctx: actionCtx,
846
- tree,
847
- result
848
- });
849
- return result;
850
- };
851
- const execute = (tree, actionCtx) => {
852
- if (actionCtx.origin === "user") noteUserActivity();
853
- return enqueue(() => {
854
- jsNodesThisDispatch = 0;
855
- return runAndEmit(tree, actionCtx);
856
- });
857
- };
858
- const canExecute = (tree, actionCtx) => {
859
- if (tree.incomplete || !tree.root) return false;
860
- const decision = decisionFor(tree.root, actionCtx.origin);
861
- return decision === "allow" || decision === "adapter";
862
- };
653
+ return { run };
654
+ }
655
+ //#endregion
656
+ //#region src/dispatch/triggers.ts
657
+ /**
658
+ * The pages whose annotations an event changes: those pages, every page
659
+ * (`'all'`: a redaction, a skipped stretch of the stream, new document
660
+ * bytes), or none.
661
+ */
662
+ const annotationPagesOf = (event) => {
663
+ switch (event.type) {
664
+ case "annotations.created":
665
+ case "annotations.updated":
666
+ case "annotations.deleted":
667
+ case "annotations.reordered":
668
+ case "annotations.flattened":
669
+ case "forms.widgetsReordered": return [event.page];
670
+ case "forms.created":
671
+ case "forms.deleted":
672
+ case "forms.widgetAdded":
673
+ case "forms.widgetRemoved":
674
+ case "forms.widgetDeleted":
675
+ case "forms.widgetRestored":
676
+ case "forms.widgetUpdated":
677
+ case "forms.restored":
678
+ case "forms.repaired": return "all";
679
+ case "pages.flattened": return event.pages;
680
+ case "redaction.applied":
681
+ case "stream.desynced":
682
+ case "document.versioned": return "all";
683
+ default: return null;
684
+ }
685
+ };
686
+ function createTriggers(ctx, { settings, form }) {
863
687
  const triggerEnabled = (trigger) => {
688
+ const { triggers } = settings.get();
864
689
  switch (trigger.scope) {
865
690
  case "activate": return true;
866
- case "annotation": return config.triggers?.annotation !== false;
867
- case "page": return config.triggers?.page !== false;
868
- case "document": return config.triggers?.document !== false;
691
+ case "annotation": return triggers.annotation;
692
+ case "page": return triggers.page;
693
+ case "document": return triggers.document;
869
694
  }
870
695
  };
871
- const lifecycleCache = /* @__PURE__ */ new Map();
872
- const lifecycleTreesFor = async (pon) => {
873
- const hit = lifecycleCache.get(pon);
874
- if (hit) return hit;
875
- const doc = ctx.doc;
876
- if (!doc) return [];
877
- const { annotations } = await doc.page(pon).annotations.list();
878
- const bearing = annotations.filter((a) => a.actions && (a.actions.pageOpen?.root || a.actions.pageClose?.root || a.actions.pageVisible?.root || a.actions.pageInvisible?.root)).map((a) => ({
879
- ref: a.ref,
880
- actions: a.actions
881
- }));
882
- lifecycleCache.set(pon, bearing);
883
- return bearing;
884
- };
885
- {
886
- const unsubscribe = ctx.doc?.events?.subscribe((event) => {
887
- if (event.type.startsWith("annotation.") || event.type === "stream.desynced") lifecycleCache.clear();
696
+ const lifecycleReads = /* @__PURE__ */ new Map();
697
+ const lifecycleAnnotationsOf = (page) => {
698
+ const key = page.objectNumber;
699
+ const cached = lifecycleReads.get(key);
700
+ if (cached) return cached;
701
+ const read = readPageAnnotations(ctx, form, page).then((annotations) => annotations.filter((annotation) => annotation.actions && (annotation.actions.pageOpen?.root || annotation.actions.pageClose?.root || annotation.actions.pageVisible?.root || annotation.actions.pageInvisible?.root)).map((annotation) => ({
702
+ ref: annotation.ref,
703
+ actions: annotation.actions
704
+ })));
705
+ lifecycleReads.set(key, read);
706
+ read.catch(() => {
707
+ if (lifecycleReads.get(key) === read) lifecycleReads.delete(key);
888
708
  });
889
- if (unsubscribe) ctx.cleanup(unsubscribe);
890
- }
709
+ return read;
710
+ };
711
+ /** Forget the pages a confirmed document event changed. */
712
+ const invalidate = (event) => {
713
+ const pages = annotationPagesOf(event);
714
+ if (pages === "all") lifecycleReads.clear();
715
+ else if (pages) for (const page of pages) lifecycleReads.delete(page.objectNumber);
716
+ };
891
717
  /**
892
- * ISO order, verified against 32000-2 Table 197 (2026-09-02): PO "shall be
893
- * executed after the O action … and the OpenAction entry"; PC "shall be
894
- * executed before the C action". Rootless trees are skipped (a
895
- * budget-degraded tree with no root has nothing to walk).
718
+ * ISO 32000-2 Table 197 order: /PO "shall be executed after the O action …
719
+ * and the OpenAction entry"; /PC "shall be executed before the C action".
720
+ * Rootless trees are skipped (a budget-degraded tree with no root has
721
+ * nothing to walk).
896
722
  */
897
- const planPageSteps = (event, pon, pageActions, lifecycle) => {
723
+ const planPageSteps = (event, page, pageActions, lifecycle) => {
898
724
  const pageSource = {
899
725
  kind: "page",
900
- pon
726
+ page
901
727
  };
902
- const annotSteps = (key) => lifecycle.filter((a) => a.actions[key]?.root).map((a) => ({
728
+ const annotationSteps = (key) => lifecycle.filter((annotation) => annotation.actions[key]?.root).map((annotation) => ({
903
729
  source: {
904
730
  kind: "annotation",
905
- annotation: a.ref,
906
- pon
731
+ annotation: annotation.ref,
732
+ page
907
733
  },
908
- tree: a.actions[key]
734
+ tree: annotation.actions[key]
909
735
  }));
910
736
  switch (event) {
911
737
  case "open": return [...pageActions?.open?.root ? [{
912
738
  source: pageSource,
913
739
  tree: pageActions.open
914
- }] : [], ...annotSteps("pageOpen")];
915
- case "close": return [...annotSteps("pageClose"), ...pageActions?.close?.root ? [{
740
+ }] : [], ...annotationSteps("pageOpen")];
741
+ case "close": return [...annotationSteps("pageClose"), ...pageActions?.close?.root ? [{
916
742
  source: pageSource,
917
743
  tree: pageActions.close
918
744
  }] : []];
919
- case "visible": return annotSteps("pageVisible");
920
- case "invisible": return annotSteps("pageInvisible");
745
+ case "visible": return annotationSteps("pageVisible");
746
+ case "invisible": return annotationSteps("pageInvisible");
921
747
  }
922
748
  };
923
- /** Aggregate step statuses. A step failure never skips its siblings; this
924
- * only FOLDS what each step reported. */
925
- const foldSteps = (steps, diagnostics) => {
926
- const statuses = steps.map((s) => s.result.status);
927
- return {
928
- status: steps.length === 0 ? "inert" : statuses.every((s) => s === "refused") ? "refused" : statuses.every((s) => s === "inert") ? "inert" : statuses.every((s) => s === "executed") ? "executed" : "partial",
929
- steps,
930
- diagnostics
931
- };
932
- };
933
- const runSteps = async (steps, origin, event, diagnostics) => {
934
- const results = [];
935
- for (const step of steps) {
936
- const result = await runAndEmit(step.tree, {
937
- origin,
938
- source: step.source,
939
- event
940
- });
941
- results.push({
942
- source: step.source,
943
- tree: step.tree,
944
- result
945
- });
946
- }
947
- return foldSteps(results, diagnostics);
749
+ return {
750
+ triggerEnabled,
751
+ lifecycleAnnotationsOf,
752
+ invalidate,
753
+ planPageSteps
948
754
  };
949
- /** Everything a trigger needs — reads included — INSIDE the queued op:
950
- * submission order is execution order. Never throws. */
951
- const resolveAndRun = async (trigger) => {
952
- const diagnostics = [];
953
- const diagnose = (diagnostic) => {
954
- diagnostics.push(diagnostic);
955
- diagnosticHook.emit(diagnostic);
956
- };
755
+ }
756
+ //#endregion
757
+ //#region src/lifecycle/document-events.ts
758
+ /**
759
+ * The verb-shaped document events (/WC, /WS, /DS, /WP, /DP) and the print
760
+ * door: one catalog lifecycle tree per event, run inside the current queue
761
+ * operation and never throwing (a broken script must not cancel the user's
762
+ * save or print), and a print request sent to the kernel's print, or, without
763
+ * a printer, to the adapter, wrapped /WP → print → /DP under the print latch.
764
+ */
765
+ function createDocumentEvents(services, { ensureOpenSequenceBeforeDocEvent }, { runAndEmit }) {
766
+ const { reportDiagnostic } = services.events;
767
+ const { readDocumentActions, DOC_EVENT_TREES } = services.catalog;
768
+ const { enqueue, budget } = services.queue;
769
+ const ports = services.ports.slots;
770
+ const { allowsPrint } = services.authority;
771
+ const { documentPrint } = services;
772
+ const printLatch = services.printLatch;
773
+ /**
774
+ * Run one catalog lifecycle tree inside the current queue operation, shared
775
+ * by the dispatch path, `runDocumentVerb` and the print wrapper. Never
776
+ * throws: a read or resolution failure degrades to a diagnostic, because a
777
+ * broken script must not cancel the user's save or print.
778
+ */
779
+ const runDocEventTreeSafe = async (event, diagnose) => {
780
+ if (!services.settings.get().triggers.document) return null;
957
781
  try {
958
- const doc = ctx.doc;
959
- if (!doc) return {
960
- status: "inert",
961
- steps: [],
962
- diagnostics
782
+ await ensureOpenSequenceBeforeDocEvent();
783
+ const tree = (await readDocumentActions())?.[DOC_EVENT_TREES[event]];
784
+ if (!tree?.root && !tree?.incomplete) return null;
785
+ return {
786
+ source: { kind: "document" },
787
+ tree,
788
+ result: await runAndEmit(tree, {
789
+ origin: "lifecycle",
790
+ source: { kind: "document" },
791
+ event: {
792
+ scope: "document",
793
+ name: event
794
+ }
795
+ })
963
796
  };
964
- if (!triggerEnabled(trigger)) {
797
+ } catch (error) {
798
+ diagnose({
799
+ code: "trigger-failed",
800
+ message: `${event}: ${error instanceof Error ? error.message : String(error)}`
801
+ });
802
+ return null;
803
+ }
804
+ };
805
+ /** The dispatch-path body for a verb-shaped document event. */
806
+ const runDocumentEventOp = async (event, diagnostics, diagnose) => {
807
+ const step = await runDocEventTreeSafe(event, diagnose);
808
+ return foldSteps(step ? [step] : [], diagnostics);
809
+ };
810
+ /**
811
+ * Both print paths (the Named Print verb, and script `doc.print()` effects
812
+ * from the actions plane) go through here. While the latch is held
813
+ * (including `runDocumentVerb('print')`'s body) a nested request is
814
+ * suppressed with `reentrant-print`.
815
+ *
816
+ * With the kernel's print, a user's request starts `documents.print()` and
817
+ * returns: the kernel runs /WP and /DP around the dialog through this
818
+ * plugin's `aroundPrint` wrap, a `runDocumentVerb('print')` that waits for
819
+ * the current queue operation, so it is never awaited here. A hover or
820
+ * lifecycle request never opens the dialog.
821
+ *
822
+ * Without a printer: /WP → `ports.uiAdapter.print` exactly once → /DP,
823
+ * with the latch reset in `finally`. An adapter throw skips /DP (the latch
824
+ * still resets). /DP fires when the adapter call returns, since a browser
825
+ * cannot observe dialog completion.
826
+ */
827
+ const firePrintThroughAdapter = async (uiContext, diagnose) => {
828
+ if (printLatch.active) {
829
+ diagnose({
830
+ code: "reentrant-print",
831
+ message: "print request during a document print event — suppressed (one dialog per request)"
832
+ });
833
+ return {
834
+ status: "blocked",
835
+ detail: "reentrant print suppressed"
836
+ };
837
+ }
838
+ if (!allowsPrint()) {
839
+ diagnose({
840
+ code: "blocked",
841
+ message: "print: doc.print is not allowed"
842
+ });
843
+ return {
844
+ status: "blocked",
845
+ detail: "doc.print is not allowed"
846
+ };
847
+ }
848
+ if (documentPrint.available()) {
849
+ if (uiContext && uiContext.origin !== "user") {
965
850
  diagnose({
966
- code: "trigger-disabled",
967
- message: `${trigger.scope}: trigger family disabled by config`
851
+ code: "blocked",
852
+ message: `print: a ${uiContext.origin} request never prints`
968
853
  });
969
854
  return {
970
- status: "inert",
971
- steps: [],
972
- diagnostics
855
+ status: "blocked",
856
+ detail: `${uiContext.origin} print request`
973
857
  };
974
858
  }
975
- const origin = require_types.originOf(trigger);
976
- switch (trigger.scope) {
977
- case "activate":
978
- case "annotation": {
979
- const event = trigger.scope === "activate" ? "activate" : trigger.event;
980
- const { annotations } = await doc.page(trigger.pon).annotations.list();
981
- const annotation = annotations.find((candidate) => sameRef(candidate.ref, trigger.ref));
982
- if (event === "mouseUp" && annotation?.actions?.activate) return {
983
- status: "inert",
984
- steps: [],
985
- diagnostics
986
- };
987
- const tree = annotation?.actions?.[event];
988
- if (!tree?.root && !tree?.incomplete) return {
989
- status: "inert",
990
- steps: [],
991
- diagnostics
992
- };
993
- const source = trigger.source ?? {
994
- kind: "annotation",
995
- annotation: trigger.ref,
996
- pon: trigger.pon
997
- };
998
- return await runSteps([{
999
- source,
1000
- tree
1001
- }], origin, require_types.eventOf(trigger), diagnostics);
1002
- }
1003
- case "page": {
1004
- const layout = ctx.document()?.pages.find((page) => page.pageObjectNumber === trigger.pon);
1005
- const lifecycle = await lifecycleTreesFor(trigger.pon);
1006
- const steps = planPageSteps(trigger.event, trigger.pon, layout?.actions, lifecycle);
1007
- return await runSteps(steps, origin, require_types.eventOf(trigger), diagnostics);
1008
- }
1009
- case "document":
1010
- if (trigger.event !== "open") return await runDocumentEventOp(trigger.event, diagnostics, diagnose);
1011
- if (openFired) {
1012
- diagnose({
1013
- code: "open-sequence-replayed",
1014
- message: "document open sequence already fired for this document"
1015
- });
1016
- return {
1017
- status: "inert",
1018
- steps: [],
1019
- diagnostics
1020
- };
1021
- }
1022
- openFired = true;
1023
- return await runOpenSequenceOp();
1024
- }
1025
- } catch (error) {
1026
- diagnose({
859
+ documentPrint.start().catch((error) => diagnose({
1027
860
  code: "trigger-failed",
1028
- message: `trigger resolution failed: ${error instanceof Error ? error.message : String(error)}`
861
+ message: `print: ${error instanceof Error ? error.message : String(error)}`
862
+ }));
863
+ return { status: "executed" };
864
+ }
865
+ if (!ports.uiAdapter) {
866
+ diagnose({
867
+ code: "no-adapter",
868
+ message: "print: no UI adapter installed"
1029
869
  });
1030
870
  return {
1031
- status: "refused",
1032
- steps: [],
1033
- diagnostics
871
+ status: "no-executor",
872
+ detail: "no UI adapter installed"
1034
873
  };
1035
874
  }
875
+ printLatch.active = true;
876
+ try {
877
+ await runDocEventTreeSafe("will-print", diagnose);
878
+ const adapter = ports.uiAdapter;
879
+ if (!adapter) return {
880
+ status: "no-executor",
881
+ detail: "UI adapter uninstalled mid-print"
882
+ };
883
+ if (uiContext) adapter.print(uiContext);
884
+ else adapter.print();
885
+ await runDocEventTreeSafe("did-print", diagnose);
886
+ return { status: "executed" };
887
+ } finally {
888
+ printLatch.active = false;
889
+ }
1036
890
  };
1037
- const dispatch = (trigger) => {
1038
- if (require_types.originOf(trigger) === "user") noteUserActivity();
1039
- return enqueue(() => {
1040
- jsNodesThisDispatch = 0;
1041
- return resolveAndRun(trigger);
1042
- });
891
+ return {
892
+ runDocumentEventOp,
893
+ firePrintThroughAdapter,
894
+ api: { runDocumentVerb: (verb, operation, options) => enqueue(async () => {
895
+ budget.scriptNodes = 0;
896
+ const diagnose = (diagnostic) => reportDiagnostic(diagnostic, { source: { kind: "document" } });
897
+ const before = verb === "save" ? "will-save" : "will-print";
898
+ const after = verb === "save" ? "did-save" : "did-print";
899
+ const body = async () => {
900
+ await runDocEventTreeSafe(before, diagnose);
901
+ if (options?.signal?.aborted) throw new _embedpdf_core.PluginError("operation-cancelled", "actions", `${verb} was cancelled`, { cause: options.signal.reason });
902
+ const value = await operation();
903
+ await runDocEventTreeSafe(after, diagnose);
904
+ return value;
905
+ };
906
+ if (verb !== "print") return body();
907
+ printLatch.active = true;
908
+ try {
909
+ return await body();
910
+ } finally {
911
+ printLatch.active = false;
912
+ }
913
+ }, options) }
1043
914
  };
1044
- const CASCADE_CAP = 8;
915
+ }
916
+ //#endregion
917
+ //#region src/lifecycle/open-sequence.ts
918
+ function createOpenSequence(services, { releaseBarrier, resetCascade }, { runAndEmit }) {
919
+ const { reportDiagnostic, openSequenceCompleted } = services.events;
920
+ /** The `openSequence` setting, read when it decides: it applies until the sequence has run. */
921
+ const mode = () => services.settings.get().openSequence;
922
+ const { readDocumentActions } = services.catalog;
923
+ const { enqueue } = services.queue;
924
+ const ports = services.ports.slots;
925
+ let openFired = false;
926
+ let sawUserActivity = false;
927
+ /** The sequence body. Runs inside the queue (through dispatch or the
928
+ * latch's own enqueue); callers guarantee `openFired` was set. */
929
+ const runOpenSequenceOp = async () => {
930
+ const diagnostics = [];
931
+ const steps = [];
932
+ const openContext = {
933
+ origin: "lifecycle",
934
+ source: { kind: "document" },
935
+ event: {
936
+ scope: "document",
937
+ name: "open"
938
+ }
939
+ };
940
+ try {
941
+ const snapshot = await readDocumentActions();
942
+ if (snapshot?.openDestination) {
943
+ const tree = {
944
+ root: {
945
+ type: "goto",
946
+ subtype: "GoTo",
947
+ destination: snapshot.openDestination,
948
+ next: []
949
+ },
950
+ incomplete: false,
951
+ warningFlags: 0,
952
+ warnings: []
953
+ };
954
+ steps.push({
955
+ source: { kind: "document" },
956
+ tree,
957
+ result: await runAndEmit(tree, openContext)
958
+ });
959
+ }
960
+ if (snapshot?.openAction?.root || snapshot?.openAction?.incomplete) steps.push({
961
+ source: { kind: "document" },
962
+ tree: snapshot.openAction,
963
+ result: await runAndEmit(snapshot.openAction, openContext)
964
+ });
965
+ } catch (error) {
966
+ const diagnostic = {
967
+ code: "trigger-failed",
968
+ message: `open sequence failed: ${error instanceof Error ? error.message : String(error)}`
969
+ };
970
+ diagnostics.push(diagnostic);
971
+ reportDiagnostic(diagnostic, { source: { kind: "document" } });
972
+ } finally {
973
+ releaseBarrier(true);
974
+ }
975
+ const result = foldSteps(steps, diagnostics);
976
+ openSequenceCompleted.emit({ result });
977
+ return result;
978
+ };
979
+ /**
980
+ * The catalog /OpenAction never runs after a /WS, /WP or /WC script: before
981
+ * the first verb-shaped document event, an armed but unfired open sequence
982
+ * runs inline (the caller is already inside the queue; `runOpenSequenceOp`
983
+ * never enqueues). It does not wait for the initial page /O: the guarantee
984
+ * is catalog-level.
985
+ */
986
+ const ensureOpenSequenceBeforeDocEvent = async () => {
987
+ if (openFired || mode() === "off") return;
988
+ openFired = true;
989
+ await runOpenSequenceOp();
990
+ };
991
+ const maybeFireOpenSequence = () => {
992
+ if (openFired) return;
993
+ if (mode() === "off") {
994
+ openFired = true;
995
+ releaseBarrier(false);
996
+ return;
997
+ }
998
+ if (!ports.uiAdapter && mode() !== "headless" && !sawUserActivity) return;
999
+ openFired = true;
1000
+ enqueue(() => runOpenSequenceOp());
1001
+ };
1002
+ const noteUserActivity = () => {
1003
+ resetCascade();
1004
+ if (!sawUserActivity) {
1005
+ sawUserActivity = true;
1006
+ maybeFireOpenSequence();
1007
+ }
1008
+ };
1009
+ /** Claim the one-shot open for a dispatched document-open trigger: false
1010
+ * when it already fired (the caller reports a replay). */
1011
+ const claim = () => {
1012
+ if (openFired) return false;
1013
+ openFired = true;
1014
+ return true;
1015
+ };
1016
+ return {
1017
+ run: runOpenSequenceOp,
1018
+ ensureOpenSequenceBeforeDocEvent,
1019
+ noteUserActivity,
1020
+ claim,
1021
+ /** Arm the latch at bringup: fires immediately for 'headless', releases
1022
+ * the barrier for 'off', waits for an adapter or user activity for 'auto'. */
1023
+ arm: maybeFireOpenSequence,
1024
+ api: { setUiAdapter: (adapter) => {
1025
+ ports.uiAdapter = adapter;
1026
+ if (adapter) maybeFireOpenSequence();
1027
+ return () => {
1028
+ if (ports.uiAdapter === adapter) ports.uiAdapter = null;
1029
+ };
1030
+ } }
1031
+ };
1032
+ }
1033
+ //#endregion
1034
+ //#region src/lifecycle/page-lifecycle.ts
1035
+ /**
1036
+ * The lifecycle coordinator's page half: the document-open barrier and the
1037
+ * cascade budget. The stage owns page truth (it reports through
1038
+ * `reportPageState`); this owns when page-lifecycle triggers fire: nothing
1039
+ * emits before the document-open sequence has run (or was declared off or
1040
+ * headless), and emission is a diff against the last-emitted state, so
1041
+ * pre-open motion collapses to one open with no phantom close.
1042
+ */
1043
+ /** Consecutive programmatic rounds after which page-lifecycle emission is suppressed. */
1044
+ const CASCADE_LIMIT = 8;
1045
+ function createPageLifecycle(ctx, { events, settings }, { dispatch }) {
1046
+ const { reportDiagnostic } = events;
1045
1047
  let barrierOpen = false;
1046
1048
  let bufferedReport = null;
1047
1049
  let lastEmitted = {
1048
- currentPon: null,
1050
+ current: null,
1049
1051
  visible: /* @__PURE__ */ new Set()
1050
1052
  };
1051
1053
  let cascadeRounds = 0;
1052
- let openFired = false;
1053
- let sawUserActivity = false;
1054
1054
  const emitForReport = (report) => {
1055
1055
  if (report.cause === "user") cascadeRounds = 0;
1056
- const nextVisible = new Set(report.visiblePons);
1057
- const changedCurrent = report.currentPon !== lastEmitted.currentPon;
1058
- const leaving = [...lastEmitted.visible].filter((pon) => !nextVisible.has(pon));
1059
- const entering = [...nextVisible].filter((pon) => !lastEmitted.visible.has(pon));
1056
+ const current = report.currentPage === null ? null : report.currentPage.objectNumber;
1057
+ const nextVisible = new Set(report.visiblePages.map((page) => page.objectNumber));
1058
+ const changedCurrent = current !== lastEmitted.current;
1059
+ const leaving = [...lastEmitted.visible].filter((pageObjectNumber) => !nextVisible.has(pageObjectNumber));
1060
+ const entering = [...nextVisible].filter((pageObjectNumber) => !lastEmitted.visible.has(pageObjectNumber));
1060
1061
  if (!changedCurrent && leaving.length === 0 && entering.length === 0) return;
1061
- const previousCurrent = lastEmitted.currentPon;
1062
+ const previousCurrent = lastEmitted.current;
1062
1063
  lastEmitted = {
1063
- currentPon: report.currentPon,
1064
+ current,
1064
1065
  visible: nextVisible
1065
1066
  };
1066
1067
  if (report.cause === "programmatic") {
1067
1068
  cascadeRounds += 1;
1068
- if (cascadeRounds > CASCADE_CAP) {
1069
- diagnosticHook.emit({
1069
+ if (cascadeRounds > CASCADE_LIMIT) {
1070
+ reportDiagnostic({
1070
1071
  code: "cascade-budget",
1071
- message: `page-lifecycle emission suppressed: ${cascadeRounds} consecutive programmatic rounds (cap ${CASCADE_CAP})`
1072
+ message: `page-lifecycle emission suppressed: ${cascadeRounds} consecutive programmatic rounds (cap ${CASCADE_LIMIT})`
1072
1073
  });
1073
1074
  return;
1074
1075
  }
@@ -1076,22 +1077,22 @@ function createActionsCapability(ctx, config = {}) {
1076
1077
  if (changedCurrent && previousCurrent !== null) dispatch({
1077
1078
  scope: "page",
1078
1079
  event: "close",
1079
- pon: previousCurrent
1080
+ page: (0, _embedpdf_core.toPageRef)(previousCurrent)
1080
1081
  });
1081
- for (const pon of leaving) dispatch({
1082
+ for (const pageObjectNumber of leaving) dispatch({
1082
1083
  scope: "page",
1083
1084
  event: "invisible",
1084
- pon
1085
+ page: (0, _embedpdf_core.toPageRef)(pageObjectNumber)
1085
1086
  });
1086
- for (const pon of entering) dispatch({
1087
+ for (const pageObjectNumber of entering) dispatch({
1087
1088
  scope: "page",
1088
1089
  event: "visible",
1089
- pon
1090
+ page: (0, _embedpdf_core.toPageRef)(pageObjectNumber)
1090
1091
  });
1091
- if (changedCurrent && report.currentPon !== null) dispatch({
1092
+ if (changedCurrent && current !== null) dispatch({
1092
1093
  scope: "page",
1093
1094
  event: "open",
1094
- pon: report.currentPon
1095
+ page: (0, _embedpdf_core.toPageRef)(current)
1095
1096
  });
1096
1097
  };
1097
1098
  const reportPageState = (report) => {
@@ -1103,333 +1104,848 @@ function createActionsCapability(ctx, config = {}) {
1103
1104
  }
1104
1105
  emitForReport(report);
1105
1106
  };
1106
- const releaseBarrier = (fireFallback) => {
1107
- barrierOpen = true;
1108
- const report = bufferedReport;
1109
- bufferedReport = null;
1110
- try {
1111
- if (report) emitForReport(report);
1112
- else if (fireFallback && config.openSequence === "headless") {
1113
- const first = ctx.document()?.pages[0]?.pageObjectNumber;
1114
- if (first !== void 0) {
1115
- lastEmitted = {
1116
- currentPon: first,
1117
- visible: lastEmitted.visible
1118
- };
1119
- dispatch({
1120
- scope: "page",
1121
- event: "open",
1122
- pon: first
1123
- });
1124
- }
1125
- }
1126
- } catch (error) {
1127
- diagnosticHook.emit({
1128
- code: "trigger-failed",
1129
- message: `barrier release failed: ${error instanceof Error ? error.message : String(error)}`
1107
+ const releaseBarrier = (fireFallback) => {
1108
+ barrierOpen = true;
1109
+ const report = bufferedReport;
1110
+ bufferedReport = null;
1111
+ try {
1112
+ if (report) emitForReport(report);
1113
+ else if (fireFallback && settings.get().openSequence === "headless") {
1114
+ const first = ctx.document()?.pages[0]?.ref.objectNumber;
1115
+ if (first !== void 0) {
1116
+ lastEmitted = {
1117
+ current: first,
1118
+ visible: lastEmitted.visible
1119
+ };
1120
+ dispatch({
1121
+ scope: "page",
1122
+ event: "open",
1123
+ page: (0, _embedpdf_core.toPageRef)(first)
1124
+ });
1125
+ }
1126
+ }
1127
+ } catch (error) {
1128
+ reportDiagnostic({
1129
+ code: "trigger-failed",
1130
+ message: `barrier release failed: ${error instanceof Error ? error.message : String(error)}`
1131
+ });
1132
+ }
1133
+ };
1134
+ return {
1135
+ releaseBarrier,
1136
+ /** A user gesture resets the programmatic-cascade counter. */
1137
+ resetCascade: () => {
1138
+ cascadeRounds = 0;
1139
+ },
1140
+ api: { reportPageState }
1141
+ };
1142
+ }
1143
+ //#endregion
1144
+ //#region src/scripting/world.ts
1145
+ /**
1146
+ * Script world building (a page-scoped prefetch: the form as the user sees it
1147
+ * plus the annotations of the page the action runs on) and the event bridge between
1148
+ * two standards: the /AA keys are ISO 32000-2 Table 200; the event names
1149
+ * live in the JavaScript API layer (ISO 21757-1, Acrobat).
1150
+ */
1151
+ function createScriptWorld(ctx, { form }) {
1152
+ const engineColorToArray = (color) => {
1153
+ if (!color) return ["T"];
1154
+ const { r, g, b } = (0, _embedpdf_engine_core_runtime.rgbOf)(color);
1155
+ return [
1156
+ "RGB",
1157
+ r / 255,
1158
+ g / 255,
1159
+ b / 255
1160
+ ];
1161
+ };
1162
+ const scriptWorldFor = async (pageObjectNumber) => {
1163
+ const page = (0, _embedpdf_engine_core_runtime.toPageRef)(pageObjectNumber);
1164
+ const snapshot = await form.read();
1165
+ const layout = ctx.getPage(page);
1166
+ const pageIndex = Math.max(0, layout?.index ?? -1);
1167
+ const { annotations } = await ctx.doc.page(page).annotations.list();
1168
+ const annots = annotations.filter((annotation) => annotation.subtype !== "link" && !annotation.subtype.startsWith("widget")).map((annotation) => {
1169
+ const inFile = layout ? (0, _embedpdf_engine_core_runtime.pdfRectOf)(annotation.rect, layout.pdfCropBox) : null;
1170
+ const styled = annotation;
1171
+ return {
1172
+ ref: annotation.ref,
1173
+ name: annotation.nm ?? "",
1174
+ subtype: annotation.subtype,
1175
+ page: pageIndex,
1176
+ rect: inFile ? [
1177
+ inFile.left,
1178
+ inFile.bottom,
1179
+ inFile.right,
1180
+ inFile.top
1181
+ ] : [
1182
+ 0,
1183
+ 0,
1184
+ 0,
1185
+ 0
1186
+ ],
1187
+ contents: annotation.contents ?? "",
1188
+ author: annotation.author ?? "",
1189
+ subject: annotation.subject ?? "",
1190
+ strokeColor: engineColorToArray(styled.color),
1191
+ fillColor: engineColorToArray(styled.interiorColor),
1192
+ opacity: styled.opacity ?? 1,
1193
+ width: styled.strokeWidth ?? 1,
1194
+ borderStyle: styled.borderStyle === "dashed" ? "D" : "S",
1195
+ dash: styled.dashArray ?? [],
1196
+ hidden: annotation.hidden,
1197
+ print: annotation.print,
1198
+ readOnly: annotation.readOnly,
1199
+ locked: annotation.locked,
1200
+ noView: annotation.noView,
1201
+ toggleNoView: annotation.toggleNoView,
1202
+ opaqueBody: annotation.subtype === "stamp"
1203
+ };
1204
+ });
1205
+ return {
1206
+ snapshot,
1207
+ world: {
1208
+ fields: (0, _embedpdf_core_acrojs.scriptFieldsFromSnapshot)(snapshot),
1209
+ annots,
1210
+ annotPages: [pageIndex],
1211
+ annotsCoverDocument: (ctx.document()?.pageCount ?? 0) <= 1
1212
+ }
1213
+ };
1214
+ };
1215
+ const ANNOTATION_EVENT_NAMES = {
1216
+ cursorEnter: "Mouse Enter",
1217
+ cursorExit: "Mouse Exit",
1218
+ mouseDown: "Mouse Down",
1219
+ mouseUp: "Mouse Up",
1220
+ focus: "Focus",
1221
+ blur: "Blur"
1222
+ };
1223
+ const PAGE_EVENT_NAMES = {
1224
+ open: "Open",
1225
+ close: "Close",
1226
+ visible: "Visible",
1227
+ invisible: "Invisible"
1228
+ };
1229
+ const DOC_EVENT_NAMES = {
1230
+ open: "Open",
1231
+ "will-save": "WillSave",
1232
+ "did-save": "DidSave",
1233
+ "will-print": "WillPrint",
1234
+ "did-print": "DidPrint",
1235
+ "will-close": "WillClose"
1236
+ };
1237
+ const scriptEventFor = (actionContext, snapshot) => {
1238
+ const provenance = actionContext.event;
1239
+ const type = provenance.scope === "page" ? "Page" : provenance.scope === "document" ? "Doc" : actionContext.source.kind === "widget" ? "Field" : actionContext.source.kind === "link" ? "Link" : actionContext.source.kind === "outline" ? "Bookmark" : "Annot";
1240
+ const name = provenance.scope === "activate" ? "Mouse Up" : provenance.scope === "annotation" ? ANNOTATION_EVENT_NAMES[provenance.name] ?? "Mouse Up" : provenance.scope === "page" ? PAGE_EVENT_NAMES[provenance.name] ?? "Open" : DOC_EVENT_NAMES[provenance.name] ?? "Open";
1241
+ const targetRef = actionContext.source.kind === "widget" ? actionContext.source.field : void 0;
1242
+ const targetField = targetRef ? snapshot.fields.find((field) => targetRef.kind === "objectNumber" && field.ref.kind === "objectNumber" ? field.ref.objectNumber === targetRef.objectNumber : targetRef.kind === "fqn" && field.name === targetRef.name) : void 0;
1243
+ return {
1244
+ kind: "widget-activate",
1245
+ type,
1246
+ name,
1247
+ ...targetRef ? {
1248
+ target: targetRef,
1249
+ source: targetRef
1250
+ } : {},
1251
+ ...targetField && targetField.valueEntry.kind === "scalar" ? { value: targetField.valueEntry.value } : {}
1252
+ };
1253
+ };
1254
+ return {
1255
+ scriptWorldFor,
1256
+ scriptEventFor
1257
+ };
1258
+ }
1259
+ //#endregion
1260
+ //#region src/scripting/executor.ts
1261
+ /** The default cap on JavaScript nodes one dispatch may run. */
1262
+ const DEFAULT_SCRIPT_NODES_PER_DISPATCH = 16;
1263
+ function registerScriptExecutor(ctx, services, environment, { scriptHost }, { performSubmit }, { surfaceScriptResult }, documentEvents) {
1264
+ const { reportDiagnostic } = services.events;
1265
+ const ports = services.ports.slots;
1266
+ const { budget } = services.queue;
1267
+ const { firePrintThroughAdapter } = documentEvents;
1268
+ const { scriptWorldFor, scriptEventFor } = createScriptWorld(ctx, services);
1269
+ /** Commit one run's document effects through the owner sinks in the
1270
+ * declared order (form first, then annotations); the first failure skips
1271
+ * the rest across both streams. Returns the failure summary, if any. */
1272
+ const commitScriptOutput = async (output, diagnose) => {
1273
+ let failure = null;
1274
+ if (output.formEffects.length > 0) if (!ports.formCommitSink) diagnose({
1275
+ code: "no-commit-sink",
1276
+ message: `script form effects dropped: no form commit sink registered (${output.formEffects.length})`
1277
+ });
1278
+ else {
1279
+ const result = await ports.formCommitSink(output.formEffects);
1280
+ if (result.status === "failed") failure = `form effects: ${result.error ?? "failed"}`;
1281
+ }
1282
+ if (output.annotEffects.length > 0) if (failure) diagnose({
1283
+ code: "executor-failed",
1284
+ message: `script annot effects skipped after form failure (${output.annotEffects.length})`
1285
+ });
1286
+ else if (!ports.annotCommitSink) diagnose({
1287
+ code: "no-commit-sink",
1288
+ message: `script annot effects dropped: no annotation commit sink registered (${output.annotEffects.length})`
1289
+ });
1290
+ else {
1291
+ const entries = output.annotEffects.map((effect) => ({
1292
+ annotObjectNumber: effect.ref.kind === "objectNumber" ? effect.ref.objectNumber : -1,
1293
+ ...effect.ref.kind === "objectNumber" ? { page: effect.ref.page } : {},
1294
+ patch: effect.patch
1295
+ }));
1296
+ const firstFailure = (await ports.annotCommitSink(entries)).results.find((entry) => entry.status === "failed");
1297
+ if (firstFailure) failure = `annotation ${firstFailure.annotObjectNumber}: ${firstFailure.error ?? "failed"}`;
1298
+ }
1299
+ if (failure) diagnose({
1300
+ code: "executor-failed",
1301
+ message: `script commit: ${failure}`
1302
+ });
1303
+ return failure;
1304
+ };
1305
+ if (!scriptHost) return;
1306
+ const nodeLimit = environment.maxScriptNodesPerDispatch ?? DEFAULT_SCRIPT_NODES_PER_DISPATCH;
1307
+ ports.executors.set("javascript", async (node, actionContext) => {
1308
+ if (node.type !== "javascript") return {
1309
+ status: "inert",
1310
+ reason: "not a JS node"
1311
+ };
1312
+ if (budget.scriptNodes >= nodeLimit) return {
1313
+ status: "inert",
1314
+ reason: `dispatch script budget exhausted (${nodeLimit} JS nodes)`
1315
+ };
1316
+ budget.scriptNodes += 1;
1317
+ const source = actionContext.source;
1318
+ const firstPage = ctx.document()?.pages[0]?.ref.objectNumber;
1319
+ const pageObjectNumber = source.kind === "widget" || source.kind === "page" ? source.page.objectNumber : source.kind === "link" || source.kind === "annotation" ? source.page?.objectNumber ?? firstPage : firstPage;
1320
+ if (pageObjectNumber === void 0) return {
1321
+ status: "inert",
1322
+ reason: "no page to anchor the world on"
1323
+ };
1324
+ const diagnose = (diagnostic) => reportDiagnostic(diagnostic, {
1325
+ action: "javascript",
1326
+ source
1327
+ });
1328
+ const pendingExternal = [];
1329
+ const splitExternal = (output, phase) => output.uiEffects.filter((effect) => {
1330
+ if (effect.kind === "print" || effect.kind === "submitForm") {
1331
+ pendingExternal.push({
1332
+ effect,
1333
+ phase
1334
+ });
1335
+ return false;
1336
+ }
1337
+ return true;
1338
+ });
1339
+ const outcome = await scriptHost.transaction(async (transaction) => {
1340
+ let built = await scriptWorldFor(pageObjectNumber);
1341
+ const boot = await transaction.boot({
1342
+ ...built.world,
1343
+ event: {
1344
+ kind: "name-tree-boot",
1345
+ type: "Doc",
1346
+ name: "Open"
1347
+ }
1348
+ });
1349
+ if (boot) {
1350
+ await commitScriptOutput(boot, diagnose);
1351
+ surfaceScriptResult({
1352
+ uiEffects: splitExternal(boot, "boot"),
1353
+ diagnostics: boot.diagnostics,
1354
+ ...boot.error ? { error: boot.error } : {},
1355
+ origin: actionContext.origin,
1356
+ phase: "boot",
1357
+ realm: "document",
1358
+ source
1359
+ });
1360
+ if (boot.formEffects.length || boot.annotEffects.length) built = await scriptWorldFor(pageObjectNumber);
1361
+ }
1362
+ const output = await transaction.run(node.script, {
1363
+ ...built.world,
1364
+ event: scriptEventFor(actionContext, built.snapshot)
1365
+ });
1366
+ surfaceScriptResult({
1367
+ uiEffects: splitExternal(output, "user"),
1368
+ diagnostics: output.diagnostics,
1369
+ ...output.error ? { error: output.error } : {},
1370
+ origin: actionContext.origin,
1371
+ phase: "user",
1372
+ realm: "document",
1373
+ source
1374
+ });
1375
+ if (output.error) return {
1376
+ status: "failed",
1377
+ error: output.error.message
1378
+ };
1379
+ const failure = await commitScriptOutput(output, diagnose);
1380
+ return failure ? {
1381
+ status: "failed",
1382
+ error: failure
1383
+ } : { status: "executed" };
1384
+ });
1385
+ for (const entry of pendingExternal) if (entry.effect.kind === "print") await firePrintThroughAdapter({
1386
+ origin: actionContext.origin,
1387
+ phase: entry.phase
1388
+ }, diagnose);
1389
+ else await performSubmit(intentOfSubmitEffect(entry.effect), actionContext, diagnose);
1390
+ return outcome;
1391
+ });
1392
+ }
1393
+ //#endregion
1394
+ //#region src/scripting/environment.ts
1395
+ function createScriptRealmFactory(environment, identity, identityDoc) {
1396
+ const sandboxFactory = environment.sandboxFactory ?? (() => import("@embedpdf/core-js-sandbox").then(({ createQuickJsSandbox }) => createQuickJsSandbox()));
1397
+ const budget = environment.budget ?? _embedpdf_core_acrojs.DEFAULT_SCRIPT_BUDGET;
1398
+ return {
1399
+ budget,
1400
+ realmFor: (target) => (0, _embedpdf_core_acrojs.createScriptHost)({
1401
+ sandboxFactory,
1402
+ document: () => {
1403
+ const documentInfo = target.document();
1404
+ return {
1405
+ id: target.doc.id,
1406
+ fileName: environment.fileName?.() ?? documentInfo?.name ?? "document.pdf",
1407
+ pageCount: documentInfo?.pageCount ?? 0,
1408
+ pageNumber: 0
1409
+ };
1410
+ },
1411
+ identity: () => (0, _embedpdf_core_acrojs.resolveScriptIdentity)(identityDoc, identity()),
1412
+ environment: (sequence) => {
1413
+ const nowMs = environment.now?.() ?? Date.now();
1414
+ return {
1415
+ nowMs,
1416
+ utcOffsetMinutes: environment.utcOffsetMinutes?.() ?? -new Date(nowMs).getTimezoneOffset(),
1417
+ randomSeed: environment.randomSeed?.() ?? (0, _embedpdf_core_acrojs.seedFrom)(target.doc.id, sequence)
1418
+ };
1419
+ },
1420
+ bootSources: target.bootSources,
1421
+ budget
1422
+ })
1423
+ };
1424
+ }
1425
+ //#endregion
1426
+ //#region src/scripting/realm.ts
1427
+ /**
1428
+ * The one ScriptHost per document. The `javascript.enabled` setting, as it is
1429
+ * when the document opens, decides whether a realm factory exists; the
1430
+ * factory carries the session environment and mints this document's own
1431
+ * realm here, and detached realms on demand (host lens): one environment, any
1432
+ * number of isolated realms.
1433
+ */
1434
+ function createRealm(ctx, { catalog, settings }, environment) {
1435
+ const { readDocumentActions } = catalog;
1436
+ const realms = settings.get().javascript.enabled ? createScriptRealmFactory(environment, () => settings.get().javascript.identity ?? void 0, ctx.doc) : null;
1437
+ const scriptHost = realms ? realms.realmFor({
1438
+ doc: ctx.doc,
1439
+ document: () => ctx.document(),
1440
+ bootSources: async () => {
1441
+ return ((await readDocumentActions())?.nameTreeScripts ?? []).map(({ action }) => (0, _embedpdf_core_acrojs.javaScriptProgramFromActionTree)(action));
1442
+ }
1443
+ }) : null;
1444
+ if (scriptHost) ctx.cleanup(() => scriptHost.dispose());
1445
+ return {
1446
+ scriptHost,
1447
+ api: {
1448
+ isScriptingEnabled: () => scriptHost !== null,
1449
+ ...scriptHost && realms ? {
1450
+ scriptRealm: {
1451
+ transaction: (body) => scriptHost.transaction(body),
1452
+ budget: realms.budget
1453
+ },
1454
+ createDetachedScriptRealm: (target) => {
1455
+ const host = realms.realmFor(target);
1456
+ return {
1457
+ transaction: (body) => host.transaction(body),
1458
+ budget: realms.budget,
1459
+ dispose: () => host.dispose()
1460
+ };
1461
+ }
1462
+ } : {}
1463
+ }
1464
+ };
1465
+ }
1466
+ //#endregion
1467
+ //#region src/scripting/surface.ts
1468
+ function createScriptSurface(services, { performSubmit }) {
1469
+ const { reportDiagnostic, scriptDiagnosticReported, scriptFailed } = services.events;
1470
+ const ports = services.ports.slots;
1471
+ const { allowsPrint } = services.authority;
1472
+ const { documentPrint } = services;
1473
+ const printLatch = services.printLatch;
1474
+ const surfaceScriptResult = (result) => {
1475
+ const uiContext = {
1476
+ origin: result.origin,
1477
+ phase: result.phase
1478
+ };
1479
+ const source = result.source ?? null;
1480
+ for (const effect of result.uiEffects) {
1481
+ if (result.realm === "detached" && effect.kind !== "alert") {
1482
+ scriptDiagnosticReported.emit({
1483
+ code: "ui-effect-suppressed",
1484
+ message: `script ${effect.kind} request from a detached realm suppressed: no document surface`
1485
+ });
1486
+ continue;
1487
+ }
1488
+ if (effect.kind === "submitForm") {
1489
+ performSubmit(intentOfSubmitEffect(effect), {
1490
+ origin: result.origin,
1491
+ source: result.source ?? { kind: "api" },
1492
+ event: { scope: "activate" }
1493
+ }, (diagnostic) => reportDiagnostic(diagnostic, {
1494
+ action: "javascript",
1495
+ source
1496
+ }));
1497
+ continue;
1498
+ }
1499
+ if (effect.kind === "print" && !allowsPrint()) {
1500
+ scriptDiagnosticReported.emit({
1501
+ code: "ui-effect-suppressed",
1502
+ message: "script print request withheld: doc.print is not allowed"
1503
+ });
1504
+ continue;
1505
+ }
1506
+ if (effect.kind === "print" && printLatch.active) {
1507
+ scriptDiagnosticReported.emit({
1508
+ code: "ui-effect-suppressed",
1509
+ message: "script print request during a document print event — suppressed (reentrant)"
1510
+ });
1511
+ continue;
1512
+ }
1513
+ if (effect.kind === "print" && documentPrint.available()) {
1514
+ if (result.origin !== "user") {
1515
+ scriptDiagnosticReported.emit({
1516
+ code: "ui-effect-suppressed",
1517
+ message: `script print request from a ${result.origin} event suppressed`
1518
+ });
1519
+ continue;
1520
+ }
1521
+ documentPrint.start().catch((error) => reportDiagnostic({
1522
+ code: "trigger-failed",
1523
+ message: `print: ${error instanceof Error ? error.message : String(error)}`
1524
+ }, {
1525
+ action: "javascript",
1526
+ source
1527
+ }));
1528
+ continue;
1529
+ }
1530
+ if (!ports.uiAdapter) {
1531
+ reportDiagnostic({
1532
+ code: "no-adapter",
1533
+ message: `script ${effect.kind}: no UI adapter installed`
1534
+ }, {
1535
+ action: "javascript",
1536
+ source
1537
+ });
1538
+ continue;
1539
+ }
1540
+ if (effect.kind === "alert") ports.uiAdapter.alert?.(effect.message, {
1541
+ ...uiContext,
1542
+ icon: effect.icon,
1543
+ ...effect.title !== void 0 ? { title: effect.title } : {}
1544
+ });
1545
+ else if (effect.kind === "gotoPage") ports.uiAdapter.gotoPage?.(effect.page, uiContext);
1546
+ else ports.uiAdapter.print(uiContext);
1547
+ }
1548
+ for (const diagnostic of result.diagnostics) scriptDiagnosticReported.emit(diagnostic);
1549
+ if (result.error) scriptFailed.emit({
1550
+ error: result.error,
1551
+ source
1552
+ });
1553
+ };
1554
+ /** A K/V/C/F commit surfaces as up to two results: the boot phase (only
1555
+ * when it produced effects) and the user phase (always — it carries the
1556
+ * diagnostics and the error). */
1557
+ const surfaceScriptCommit = (commit, context) => {
1558
+ for (const phase of ["boot", "user"]) {
1559
+ const uiEffects = commit.uiEffects.filter((effect) => effect.phase === phase);
1560
+ if (uiEffects.length === 0 && phase === "boot") continue;
1561
+ surfaceScriptResult({
1562
+ uiEffects,
1563
+ diagnostics: phase === "user" ? commit.diagnostics : [],
1564
+ ...phase === "user" && commit.error ? { error: commit.error } : {},
1565
+ origin: context.origin,
1566
+ phase,
1567
+ realm: context.realm,
1568
+ ...context.source ? { source: context.source } : {}
1569
+ });
1570
+ }
1571
+ };
1572
+ return {
1573
+ surfaceScriptResult,
1574
+ api: {
1575
+ surfaceScriptResult,
1576
+ surfaceScriptCommit
1577
+ }
1578
+ };
1579
+ }
1580
+ //#endregion
1581
+ //#region src/services/authority.ts
1582
+ function createAuthority(ctx) {
1583
+ const allowsPrint = () => ctx.allows("doc.print");
1584
+ return { allowsPrint };
1585
+ }
1586
+ //#endregion
1587
+ //#region src/services/catalog.ts
1588
+ /** The ISO 32000-2 Table 200 key for each verb-shaped document trigger event. */
1589
+ const DOC_EVENT_TREES = {
1590
+ "will-save": "willSave",
1591
+ "did-save": "didSave",
1592
+ "will-print": "willPrint",
1593
+ "did-print": "didPrint",
1594
+ "will-close": "willClose"
1595
+ };
1596
+ function createCatalog(ctx) {
1597
+ let snapshotRead = null;
1598
+ const readDocumentActions = () => {
1599
+ if (!snapshotRead) {
1600
+ const service = ctx.doc.actions;
1601
+ const memoized = (service ? Promise.resolve(service.get()) : Promise.resolve(null)).catch((error) => {
1602
+ if (snapshotRead === memoized) snapshotRead = null;
1603
+ throw error;
1604
+ });
1605
+ snapshotRead = memoized;
1606
+ }
1607
+ return snapshotRead;
1608
+ };
1609
+ return {
1610
+ readDocumentActions,
1611
+ DOC_EVENT_TREES
1612
+ };
1613
+ }
1614
+ //#endregion
1615
+ //#region src/services/document-print.ts
1616
+ /**
1617
+ * The kernel's print, where the document's own print requests go (the Named Print verb, a
1618
+ * script's `print()`) when the host gave the kernel a printer. The kernel prints the
1619
+ * document's pages, and runs the document's /WP and /DP around the dialog through the wrap
1620
+ * this plugin registers (`ctx.aroundPrint`). Without a printer, requests fall back to the UI
1621
+ * adapter's `print`.
1622
+ */
1623
+ function createDocumentPrint(ctx) {
1624
+ const documents = () => ctx.get(_embedpdf_core.DocumentsToken);
1625
+ return {
1626
+ /** Whether the kernel prints this document: `doc.print`, and the host gave a printer. */
1627
+ available: () => documents().canPrint(),
1628
+ /** Print the document through the kernel. */
1629
+ start: () => documents().print()
1630
+ };
1631
+ }
1632
+ //#endregion
1633
+ //#region src/services/events.ts
1634
+ function createEvents(ctx) {
1635
+ const executed = ctx.events.source();
1636
+ const diagnosticReported = ctx.events.source();
1637
+ return {
1638
+ executed,
1639
+ diagnosticReported,
1640
+ scriptDiagnosticReported: ctx.events.source(),
1641
+ scriptFailed: ctx.events.source(),
1642
+ openSequenceCompleted: ctx.events.source(),
1643
+ /** Fire `onDiagnosticReported` with the action and source it concerns (`null` for none). */
1644
+ reportDiagnostic(diagnostic, place = {}) {
1645
+ diagnosticReported.emit({
1646
+ code: diagnostic.code,
1647
+ action: place.action ?? null,
1648
+ source: place.source ?? null
1130
1649
  });
1131
1650
  }
1132
1651
  };
1133
- /** The §3.9 sequence body — runs INSIDE the queue (via dispatch or the
1134
- * latch's own enqueue; callers guarantee openFired was set). */
1135
- const runOpenSequenceOp = async () => {
1136
- const diagnostics = [];
1137
- const steps = [];
1138
- const lifecycleCtx = {
1139
- origin: "lifecycle",
1140
- source: { kind: "document" },
1141
- event: {
1142
- scope: "document",
1143
- name: "open"
1144
- }
1145
- };
1146
- try {
1147
- const snapshot = await readDocumentActions();
1148
- if (snapshot?.openDestination) {
1149
- const tree = {
1150
- root: {
1151
- type: "goto",
1152
- subtype: "GoTo",
1153
- destination: snapshot.openDestination,
1154
- next: []
1155
- },
1156
- incomplete: false,
1157
- warningFlags: 0,
1158
- warnings: []
1652
+ }
1653
+ //#endregion
1654
+ //#region src/services/form.ts
1655
+ function createForm(ctx, ports) {
1656
+ return { read: (read = (task) => task) => {
1657
+ const shown = ports.formReader?.();
1658
+ return shown ? Promise.resolve(shown) : read(ctx.doc.forms.list());
1659
+ } };
1660
+ }
1661
+ //#endregion
1662
+ //#region src/services/ports.ts
1663
+ function createPorts({ reportDiagnostic }) {
1664
+ const ports = {
1665
+ executors: /* @__PURE__ */ new Map(),
1666
+ annotCommitSink: null,
1667
+ formCommitSink: null,
1668
+ formReader: null,
1669
+ uiAdapter: null,
1670
+ submitHandler: null,
1671
+ submitResolver: null
1672
+ };
1673
+ return {
1674
+ slots: ports,
1675
+ api: {
1676
+ registerExecutor: (type, executor) => {
1677
+ if (ports.executors.has(type)) reportDiagnostic({
1678
+ code: "duplicate-executor",
1679
+ message: `executor for '${type}' replaced (last-wins)`
1680
+ }, { action: type });
1681
+ ports.executors.set(type, executor);
1682
+ return () => {
1683
+ if (ports.executors.get(type) === executor) ports.executors.delete(type);
1684
+ };
1685
+ },
1686
+ registerAnnotCommitSink: (sink) => {
1687
+ ports.annotCommitSink = sink;
1688
+ return () => {
1689
+ if (ports.annotCommitSink === sink) ports.annotCommitSink = null;
1690
+ };
1691
+ },
1692
+ registerFormCommitSink: (sink) => {
1693
+ ports.formCommitSink = sink;
1694
+ return () => {
1695
+ if (ports.formCommitSink === sink) ports.formCommitSink = null;
1696
+ };
1697
+ },
1698
+ registerFormReader: (reader) => {
1699
+ ports.formReader = reader;
1700
+ return () => {
1701
+ if (ports.formReader === reader) ports.formReader = null;
1702
+ };
1703
+ },
1704
+ setSubmitHandler: (handler) => {
1705
+ ports.submitHandler = handler;
1706
+ return () => {
1707
+ if (ports.submitHandler === handler) ports.submitHandler = null;
1708
+ };
1709
+ },
1710
+ registerSubmitResolver: (resolver) => {
1711
+ ports.submitResolver = resolver;
1712
+ return () => {
1713
+ if (ports.submitResolver === resolver) ports.submitResolver = null;
1159
1714
  };
1160
- steps.push({
1161
- source: { kind: "document" },
1162
- tree,
1163
- result: await runAndEmit(tree, lifecycleCtx)
1164
- });
1165
1715
  }
1166
- if (snapshot?.openAction?.root || snapshot?.openAction?.incomplete) steps.push({
1167
- source: { kind: "document" },
1168
- tree: snapshot.openAction,
1169
- result: await runAndEmit(snapshot.openAction, lifecycleCtx)
1170
- });
1171
- } catch (error) {
1172
- const diagnostic = {
1173
- code: "trigger-failed",
1174
- message: `open sequence failed: ${error instanceof Error ? error.message : String(error)}`
1175
- };
1176
- diagnostics.push(diagnostic);
1177
- diagnosticHook.emit(diagnostic);
1178
- } finally {
1179
- releaseBarrier(true);
1180
1716
  }
1181
- return foldSteps(steps, diagnostics);
1182
1717
  };
1183
- /**
1184
- * D1's open-ordering law: catalog `OpenAction` may never run AFTER a
1185
- * WillSave/WillPrint/WillClose script. Before the first verb-shaped
1186
- * document event, an armed-but-unfired open sequence runs inline (we are
1187
- * already inside the queue; `runOpenSequenceOp` never enqueues). It does
1188
- * NOT wait for the initial page `/O` — the guarantee is catalog-level.
1189
- */
1190
- const ensureOpenSequenceBeforeDocEvent = async () => {
1191
- if (openFired || config.openSequence === "off") return;
1192
- openFired = true;
1193
- await runOpenSequenceOp();
1718
+ }
1719
+ //#endregion
1720
+ //#region src/services/print-latch.ts
1721
+ const createPrintLatch = () => ({ active: false });
1722
+ //#endregion
1723
+ //#region src/services/queue.ts
1724
+ function createQueue(ctx) {
1725
+ return {
1726
+ enqueue: ctx.serialQueue("actions"),
1727
+ budget: { scriptNodes: 0 }
1728
+ };
1729
+ }
1730
+ //#endregion
1731
+ //#region src/services/index.ts
1732
+ function createServices(ctx) {
1733
+ const events = createEvents(ctx);
1734
+ const ports = createPorts(events);
1735
+ return {
1736
+ settings: ctx.settings(),
1737
+ events,
1738
+ policy: createPolicy(ctx),
1739
+ ports,
1740
+ queue: createQueue(ctx),
1741
+ catalog: createCatalog(ctx),
1742
+ form: createForm(ctx, ports.slots),
1743
+ authority: createAuthority(ctx),
1744
+ documentPrint: createDocumentPrint(ctx),
1745
+ printLatch: createPrintLatch()
1194
1746
  };
1747
+ }
1748
+ //#endregion
1749
+ //#region src/submit/perform.ts
1750
+ function createSubmit(ctx, services, environment) {
1751
+ const policy = services.policy;
1752
+ const ports = services.ports.slots;
1753
+ const { reportDiagnostic } = services.events;
1754
+ const nowMs = () => environment.now?.() ?? Date.now();
1195
1755
  /**
1196
- * Run ONE catalog lifecycle tree inside the current queue operation —
1197
- * shared by the dispatch path, `runDocumentVerb`, and the print wrapper.
1198
- * Never throws: a read/resolution failure degrades to a diagnostic (a
1199
- * broken script must not cancel the user's save/print — D2).
1756
+ * The one submit door. Sink order: the embedder handler (explicit beats
1757
+ * ambient; its promise is not awaited) → the document's home
1758
+ * (`doc.forms.submit`, engine-asserted and awaited: a real operation with a
1759
+ * real receipt) → blocked with `no-submit-sink`.
1200
1760
  */
1201
- const runDocEventTreeSafe = async (event, diagnose) => {
1202
- if (config.triggers?.document === false) return null;
1203
- try {
1204
- await ensureOpenSequenceBeforeDocEvent();
1205
- const tree = (await readDocumentActions())?.[DOC_EVENT_TREES[event]];
1206
- if (!tree?.root && !tree?.incomplete) return null;
1207
- return {
1208
- source: { kind: "document" },
1209
- tree,
1210
- result: await runAndEmit(tree, {
1211
- origin: "lifecycle",
1212
- source: { kind: "document" },
1213
- event: {
1214
- scope: "document",
1215
- name: event
1216
- }
1217
- })
1218
- };
1219
- } catch (error) {
1220
- diagnose({
1221
- code: "trigger-failed",
1222
- message: `${event}: ${error instanceof Error ? error.message : String(error)}`
1761
+ const performSubmit = async (intent, actionContext, diagnose) => {
1762
+ const decision = policy.current()["submit-form"][actionContext.origin];
1763
+ if (decision !== "adapter" && decision !== "allow") {
1764
+ if (decision === "report") diagnose({
1765
+ code: "blocked",
1766
+ message: `submit-form: policy 'report' for origin '${actionContext.origin}'`
1223
1767
  });
1224
- return null;
1768
+ return { status: "blocked" };
1225
1769
  }
1226
- };
1227
- /** The dispatch-path body for a verb-shaped document event. */
1228
- const runDocumentEventOp = async (event, diagnostics, diagnose) => {
1229
- const step = await runDocEventTreeSafe(event, diagnose);
1230
- return foldSteps(step ? [step] : [], diagnostics);
1231
- };
1232
- /**
1233
- * D3: BOTH adapter print invocations (the Named Print verb; script
1234
- * `doc.print()` effects from the actions plane) go through here — WP →
1235
- * `uiAdapter.print` exactly once → DP, latch reset in `finally`. While
1236
- * the latch is held (including `runDocumentVerb('print')`'s body) a
1237
- * nested request is suppressed with `reentrant-print`. An adapter throw
1238
- * skips DP (the latch still resets) — named deviation: DP otherwise
1239
- * fires when the adapter call RETURNS, since a browser cannot observe
1240
- * dialog completion.
1241
- */
1242
- const firePrintThroughAdapter = async (uiContext, diagnose) => {
1243
- if (docPrintEventActive) {
1770
+ if (!ports.submitResolver) {
1244
1771
  diagnose({
1245
- code: "reentrant-print",
1246
- message: "print request during a document print event — suppressed (one dialog per request)"
1772
+ code: "no-submit-resolver",
1773
+ message: "submit-form: no dataset resolver registered (form plugin missing?)"
1247
1774
  });
1248
1775
  return {
1249
1776
  status: "blocked",
1250
- detail: "reentrant print suppressed"
1777
+ detail: "no dataset resolver"
1251
1778
  };
1252
1779
  }
1253
- if (!allowsPrint()) {
1780
+ let request;
1781
+ try {
1782
+ request = await ports.submitResolver(intent, actionContext, diagnose);
1783
+ } catch (error) {
1784
+ const detail = error instanceof Error ? error.message : String(error);
1254
1785
  diagnose({
1255
- code: "blocked",
1256
- message: "print: doc.print is not allowed"
1786
+ code: "executor-failed",
1787
+ message: `submit-form: dataset resolution: ${detail}`
1257
1788
  });
1258
1789
  return {
1259
- status: "blocked",
1260
- detail: "doc.print is not allowed"
1790
+ status: "failed",
1791
+ detail
1261
1792
  };
1262
1793
  }
1263
- if (!uiAdapter) {
1794
+ const forms = ctx.doc.forms;
1795
+ const homeSubmit = typeof forms.submit === "function" ? () => Promise.resolve(forms.submit(toFormSubmissionRequest(request, new Date(nowMs()).toISOString()))) : null;
1796
+ if (ports.submitHandler) try {
1797
+ const outcome = ports.submitHandler(request, { submitToDocumentHome: homeSubmit });
1798
+ if (outcome && typeof outcome.then === "function") outcome.catch((error) => {
1799
+ reportDiagnostic({
1800
+ code: "executor-failed",
1801
+ message: `submit handler rejected (detached): ${error instanceof Error ? error.message : String(error)}`
1802
+ }, {
1803
+ action: "submit-form",
1804
+ source: actionContext.source
1805
+ });
1806
+ });
1807
+ return { status: "executed" };
1808
+ } catch (error) {
1809
+ const detail = error instanceof Error ? error.message : String(error);
1264
1810
  diagnose({
1265
- code: "no-adapter",
1266
- message: "print: no UI adapter installed"
1811
+ code: "executor-failed",
1812
+ message: `submit handler threw: ${detail}`
1267
1813
  });
1268
1814
  return {
1269
- status: "no-executor",
1270
- detail: "no UI adapter installed"
1815
+ status: "failed",
1816
+ detail
1271
1817
  };
1272
1818
  }
1273
- docPrintEventActive = true;
1274
- try {
1275
- await runDocEventTreeSafe("will-print", diagnose);
1276
- const adapter = uiAdapter;
1277
- if (!adapter) return {
1278
- status: "no-executor",
1279
- detail: "UI adapter uninstalled mid-print"
1280
- };
1281
- if (uiContext) adapter.print(uiContext);
1282
- else adapter.print();
1283
- await runDocEventTreeSafe("did-print", diagnose);
1819
+ if (homeSubmit) try {
1820
+ await homeSubmit();
1284
1821
  return { status: "executed" };
1285
- } finally {
1286
- docPrintEventActive = false;
1287
- }
1288
- };
1289
- const maybeFireOpenSequence = () => {
1290
- if (openFired) return;
1291
- if (config.openSequence === "off") {
1292
- openFired = true;
1293
- releaseBarrier(false);
1294
- return;
1822
+ } catch (error) {
1823
+ const detail = error instanceof Error ? error.message : String(error);
1824
+ diagnose({
1825
+ code: "executor-failed",
1826
+ message: `document home refused submit: ${detail}`
1827
+ });
1828
+ return {
1829
+ status: "failed",
1830
+ detail
1831
+ };
1295
1832
  }
1296
- if (!uiAdapter && config.openSequence !== "headless" && !sawUserActivity) return;
1297
- openFired = true;
1298
- enqueue(() => runOpenSequenceOp());
1833
+ diagnose({
1834
+ code: "no-submit-sink",
1835
+ message: "submit-form: no handler installed and the document has no submit-capable home"
1836
+ });
1837
+ return {
1838
+ status: "blocked",
1839
+ detail: "no submit sink"
1840
+ };
1299
1841
  };
1300
- const noteUserActivity = () => {
1301
- cascadeRounds = 0;
1302
- if (!sawUserActivity) {
1303
- sawUserActivity = true;
1304
- maybeFireOpenSequence();
1305
- }
1842
+ return { performSubmit };
1843
+ }
1844
+ //#endregion
1845
+ //#region src/controller.ts
1846
+ /**
1847
+ * The actions controller: the composition root. It builds the plugin's
1848
+ * services once, wires each area with the services it declares, and
1849
+ * assembles the host capability from the areas' API slices. No behavior
1850
+ * lives here: every verb and read has a home in `submit/`, `scripting/`,
1851
+ * `dispatch/` or `lifecycle/`. The plugin keeps no session state: its rules
1852
+ * are its settings, and its live data (ports, queue, latches) are resources
1853
+ * of the areas.
1854
+ */
1855
+ /**
1856
+ * `config` is what the app registered; the settings in it reach the
1857
+ * controller through `ctx.settings()`, and only the script environment is
1858
+ * read from it here.
1859
+ */
1860
+ function createActionsController(ctx, config = {}) {
1861
+ const services = createServices(ctx);
1862
+ const { events, ports, settings } = services;
1863
+ const core = {
1864
+ dispatch: (trigger) => dispatcher.dispatch(trigger),
1865
+ runAndEmit: (tree, actionContext) => dispatcher.runAndEmit(tree, actionContext)
1306
1866
  };
1307
- maybeFireOpenSequence();
1867
+ const environment = config.javascript ?? {};
1868
+ const submit = createSubmit(ctx, services, environment);
1869
+ const realm = createRealm(ctx, services, environment);
1870
+ const surface = createScriptSurface(services, submit);
1871
+ const pageLifecycle = createPageLifecycle(ctx, services, core);
1872
+ const openSequence = createOpenSequence(services, pageLifecycle, core);
1873
+ const documentEvents = createDocumentEvents(services, openSequence, core);
1874
+ const triggers = createTriggers(ctx, services);
1875
+ const dispatcher = createDispatcher(ctx, services, createRunner(ctx, services, submit, documentEvents), triggers, openSequence, documentEvents);
1876
+ registerScriptExecutor(ctx, services, environment, realm, submit, surface, documentEvents);
1308
1877
  return {
1309
- execute,
1310
- canExecute,
1311
- dispatch,
1312
- canDispatch: (trigger) => triggerEnabled(trigger) && ctx.doc !== null,
1313
- setUiAdapter: (adapter) => {
1314
- uiAdapter = adapter;
1315
- if (adapter) maybeFireOpenSequence();
1316
- return () => {
1317
- if (uiAdapter === adapter) uiAdapter = null;
1318
- };
1319
- },
1320
- runDocumentVerb: (verb, operation) => enqueue(async () => {
1321
- jsNodesThisDispatch = 0;
1322
- const diagnose = (diagnostic) => diagnosticHook.emit(diagnostic);
1323
- const before = verb === "save" ? "will-save" : "will-print";
1324
- const after = verb === "save" ? "did-save" : "did-print";
1325
- const body = async () => {
1326
- await runDocEventTreeSafe(before, diagnose);
1327
- const value = await operation();
1328
- await runDocEventTreeSafe(after, diagnose);
1329
- return value;
1330
- };
1331
- if (verb !== "print") return body();
1332
- docPrintEventActive = true;
1333
- try {
1334
- return await body();
1335
- } finally {
1336
- docPrintEventActive = false;
1337
- }
1338
- }),
1339
- prepareClose: () => dispatch({
1340
- scope: "document",
1341
- event: "will-close"
1342
- }),
1343
- setSubmitHandler: (handler) => {
1344
- submitHandler = handler;
1345
- return () => {
1346
- if (submitHandler === handler) submitHandler = null;
1347
- };
1348
- },
1349
- onAction: actionHook.on,
1350
- onDiagnostic: diagnosticHook.on,
1351
- onScriptDiagnostic: scriptDiagnosticHook.on,
1352
- onScriptError: scriptErrorHook.on,
1353
- registerExecutor: (type, executor) => {
1354
- if (executors.has(type)) diagnosticHook.emit({
1355
- code: "duplicate-executor",
1356
- message: `executor for '${type}' replaced (last-wins)`
1357
- });
1358
- executors.set(type, executor);
1359
- return () => {
1360
- if (executors.get(type) === executor) executors.delete(type);
1361
- };
1362
- },
1363
- registerAnnotCommitSink: (sink) => {
1364
- annotCommitSink = sink;
1365
- return () => {
1366
- if (annotCommitSink === sink) annotCommitSink = null;
1367
- };
1368
- },
1369
- registerFormCommitSink: (sink) => {
1370
- formCommitSink = sink;
1371
- return () => {
1372
- if (formCommitSink === sink) formCommitSink = null;
1373
- };
1374
- },
1375
- ...scriptHost && realms ? {
1376
- scriptRealm: {
1377
- transaction: (body) => scriptHost.transaction(body),
1378
- budget: realms.budget
1379
- },
1380
- createDetachedScriptRealm: (target) => {
1381
- const host = realms.realmFor(target);
1382
- return {
1383
- transaction: (body) => host.transaction(body),
1384
- budget: realms.budget,
1385
- dispose: () => host.dispose()
1386
- };
1878
+ api: (0, _embedpdf_core.composeApi)("actions", [
1879
+ settings.api,
1880
+ ports.api,
1881
+ realm.api,
1882
+ surface.api,
1883
+ pageLifecycle.api,
1884
+ openSequence.api,
1885
+ documentEvents.api,
1886
+ dispatcher.api,
1887
+ {
1888
+ onExecuted: events.executed.on,
1889
+ onDiagnosticReported: events.diagnosticReported.on,
1890
+ onScriptDiagnosticReported: events.scriptDiagnosticReported.on,
1891
+ onScriptFailed: events.scriptFailed.on,
1892
+ onOpenSequenceCompleted: events.openSequenceCompleted.on
1387
1893
  }
1388
- } : {},
1389
- surfaceScriptResult,
1390
- surfaceScriptCommit,
1391
- reportPageState,
1392
- registerSubmitResolver: (resolver) => {
1393
- submitResolver = resolver;
1394
- return () => {
1395
- if (submitResolver === resolver) submitResolver = null;
1396
- };
1894
+ ]),
1895
+ connect() {
1896
+ ctx.listen(ctx.doc.events, (event) => triggers.invalidate(event));
1897
+ openSequence.arm();
1898
+ ctx.aroundDownload((read) => documentEvents.api.runDocumentVerb("save", read));
1899
+ ctx.aroundPrint((print) => documentEvents.api.runDocumentVerb("print", print));
1397
1900
  }
1398
1901
  };
1399
1902
  }
1400
1903
  //#endregion
1401
- //#region src/reducer.ts
1402
- const initialActionsState = () => ({ seq: 0 });
1403
- function actionsReducer(state, action) {
1404
- switch (action.type) {
1405
- case "ACTIONS_DISPATCHED": return { seq: state.seq + 1 };
1406
- default: return state;
1407
- }
1904
+ //#region src/settings.ts
1905
+ /** `config` without the script environment, which is no setting: what the settings register. */
1906
+ function registeredSettings(config) {
1907
+ if (!config) return void 0;
1908
+ const { javascript } = config;
1909
+ return {
1910
+ policy: config.policy,
1911
+ triggers: config.triggers,
1912
+ openSequence: config.openSequence,
1913
+ javascript: javascript && {
1914
+ enabled: javascript.enabled,
1915
+ identity: javascript.identity
1916
+ }
1917
+ };
1408
1918
  }
1409
1919
  //#endregion
1410
1920
  //#region src/actions.plugin.ts
1411
1921
  /**
1412
- * The action engine: the DEPENDENCY ROOT of the action architecture. It
1922
+ * The action engine, the dependency root of the action architecture. It
1413
1923
  * interprets extracted /A and /AA trees; it never detects triggers and never
1414
- * imports another plugin's token — stage, annotation, link, and form
1415
- * optionally depend on ActionsToken and register their executors and sinks
1416
- * at init (the kernel's topological order guarantees this plugin initializes
1924
+ * imports another plugin's token: the stage, annotation, link and form
1925
+ * plugins optionally depend on it and register their executors and sinks
1926
+ * when they connect (the kernel's dependency order creates this plugin
1417
1927
  * first). JavaScript is one registered interpreter among many: Hide,
1418
- * ResetForm, GoTo, and Named work with scripting off.
1928
+ * ResetForm, GoTo and Named work with scripting off. Document-scoped and
1929
+ * stateless; its settings are what the app registered over
1930
+ * {@link ACTIONS_DEFAULTS}.
1419
1931
  */
1420
1932
  const actionsPlugin = (config) => (0, _embedpdf_core.definePlugin)({
1421
1933
  id: "actions",
1422
- token: require_types.ActionsToken,
1934
+ token: require_contract_host.ActionsToken,
1423
1935
  scope: "document",
1424
- initialState: initialActionsState,
1425
- reduce: actionsReducer,
1426
- capability: (ctx) => createActionsCapability(ctx, config)
1936
+ settings: {
1937
+ defaults: require_contract.ACTIONS_DEFAULTS,
1938
+ registered: registeredSettings(config)
1939
+ },
1940
+ create: (ctx) => createActionsController(ctx, config)
1427
1941
  });
1428
1942
  //#endregion
1429
- exports.ActionsToken = require_types.ActionsToken;
1943
+ exports.ACTIONS_DEFAULTS = require_contract.ACTIONS_DEFAULTS;
1944
+ exports.ActionsToken = require_contract.ActionsToken;
1430
1945
  exports.actionsPlugin = actionsPlugin;
1431
1946
  exports.createHoverPump = require_contract.createHoverPump;
1432
- exports.originOf = require_types.originOf;
1947
+ exports.eventOf = require_contract.eventOf;
1433
1948
  exports.submitEntriesToUrlEncoded = require_contract.submitEntriesToUrlEncoded;
1949
+ exports.triggerOriginOf = require_contract.triggerOriginOf;
1434
1950
 
1435
1951
  //# sourceMappingURL=index.cjs.map