@embedpdf/plugin-actions 3.0.0-next.13 → 3.0.0-next.15

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