@superpanel-sdk/runtime 0.5.0 → 0.7.0

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 (189) hide show
  1. package/README.md +3 -3
  2. package/dist/builder/codegen/emit.d.ts +5 -10
  3. package/dist/builder/codegen/emit.js +100 -106
  4. package/dist/builder/codegen/generate.d.ts +3 -5
  5. package/dist/builder/codegen/generate.js +13 -32
  6. package/dist/builder/company-imports.d.ts +5 -0
  7. package/dist/builder/company-imports.js +9 -1
  8. package/dist/builder/compile/compile-sequence.d.ts +13 -11
  9. package/dist/builder/compile/compile-sequence.js +135 -83
  10. package/dist/builder/compile/function-code.d.ts +15 -9
  11. package/dist/builder/compile/function-code.js +59 -29
  12. package/dist/builder/compile/registry.d.ts +4 -4
  13. package/dist/builder/compile/registry.js +14 -12
  14. package/dist/builder/errors.d.ts +9 -0
  15. package/dist/builder/errors.js +17 -0
  16. package/dist/builder/function-params.d.ts +10 -0
  17. package/dist/builder/function-params.js +203 -0
  18. package/dist/builder/load.d.ts +17 -17
  19. package/dist/builder/load.js +27 -25
  20. package/dist/builder/paths.d.ts +6 -1
  21. package/dist/builder/paths.js +16 -2
  22. package/dist/builder/schedules.d.ts +9 -7
  23. package/dist/builder/schedules.js +20 -21
  24. package/dist/builder/typecheck.d.ts +1 -1
  25. package/dist/builder/typecheck.js +3 -6
  26. package/dist/builder/types.d.ts +41 -5
  27. package/dist/builder/types.js +19 -0
  28. package/dist/cli/calls/index.d.ts +177 -0
  29. package/dist/cli/calls/index.js +96 -0
  30. package/dist/cli/debug/index.d.ts +31 -0
  31. package/dist/cli/debug/index.js +119 -0
  32. package/dist/cli/exec/index.d.ts +39 -0
  33. package/dist/cli/exec/index.js +138 -0
  34. package/dist/cli/exec/session.d.ts +48 -0
  35. package/dist/cli/exec/session.js +82 -0
  36. package/dist/cli/exec/trace.d.ts +18 -0
  37. package/dist/cli/exec/trace.js +52 -0
  38. package/dist/cli/index.js +84 -1
  39. package/dist/cli/logs/index.d.ts +47 -0
  40. package/dist/cli/logs/index.js +113 -0
  41. package/dist/cli/mobile/index.d.ts +12 -0
  42. package/dist/cli/mobile/index.js +405 -0
  43. package/dist/cli/serve/index.d.ts +40 -4
  44. package/dist/cli/serve/index.js +167 -28
  45. package/dist/cli/turns/index.d.ts +105 -0
  46. package/dist/cli/turns/index.js +128 -0
  47. package/dist/cli/turns/run.d.ts +13 -0
  48. package/dist/cli/turns/run.js +57 -0
  49. package/dist/cli/turns/shadow.d.ts +112 -0
  50. package/dist/cli/turns/shadow.js +54 -0
  51. package/dist/cli/type-gen/index.js +0 -1
  52. package/dist/compute-host/compute-host.d.ts +5 -6
  53. package/dist/compute-host/compute-host.js +4 -13
  54. package/dist/compute-host/source-compute-host.d.ts +5 -7
  55. package/dist/compute-host/source-compute-host.js +12 -33
  56. package/dist/core/case-orchestrator/contact-hours.d.ts +26 -6
  57. package/dist/core/case-orchestrator/contact-hours.js +27 -8
  58. package/dist/core/case-orchestrator/index.d.ts +1 -4
  59. package/dist/core/case-orchestrator/index.js +1 -4
  60. package/dist/core/case-orchestrator/render.d.ts +1 -1
  61. package/dist/core/case-orchestrator/render.js +15 -10
  62. package/dist/core/case-orchestrator/types.d.ts +83 -128
  63. package/dist/core/case-orchestrator/types.js +0 -4
  64. package/dist/core/context/email-message.d.ts +40 -1
  65. package/dist/core/context/email-message.js +27 -0
  66. package/dist/core/context/email.d.ts +13 -1
  67. package/dist/core/context/email.js +2 -3
  68. package/dist/core/context/exchanges.d.ts +25 -0
  69. package/dist/core/context/exchanges.js +25 -7
  70. package/dist/core/context/instances.d.ts +31 -2
  71. package/dist/core/context/instances.js +18 -4
  72. package/dist/core/context/platform-registry.d.ts +35 -0
  73. package/dist/core/context/sms.d.ts +30 -0
  74. package/dist/core/context/sms.js +20 -0
  75. package/dist/core/context/types.d.ts +29 -1
  76. package/dist/core/conversation/index.d.ts +58 -4
  77. package/dist/core/conversation/index.js +123 -4
  78. package/dist/core/functions/errors.d.ts +74 -0
  79. package/dist/core/functions/errors.js +26 -0
  80. package/dist/core/functions/events.d.ts +23 -78
  81. package/dist/core/functions/events.js +8 -17
  82. package/dist/core/functions/schedules.d.ts +9 -10
  83. package/dist/core/functions/schedules.js +2 -2
  84. package/dist/core/functions/types.d.ts +17 -72
  85. package/dist/core/functions/types.js +1 -9
  86. package/dist/core/page/define.d.ts +230 -160
  87. package/dist/core/page/define.js +145 -104
  88. package/dist/core/page/envelope.d.ts +55 -0
  89. package/dist/core/page/envelope.js +4 -0
  90. package/dist/core/page/fns.d.ts +448 -0
  91. package/dist/core/page/fns.js +539 -0
  92. package/dist/core/page/memory-reader.d.ts +27 -0
  93. package/dist/core/page/memory-reader.js +299 -0
  94. package/dist/core/page/page-host.d.ts +20 -0
  95. package/dist/core/page/page-host.js +57 -0
  96. package/dist/core/page/records-query.d.ts +234 -0
  97. package/dist/core/page/records-query.js +780 -0
  98. package/dist/core/page/render.d.ts +67 -25
  99. package/dist/core/page/render.js +492 -272
  100. package/dist/core/page/sandbox.d.ts +85 -0
  101. package/dist/core/page/sandbox.js +357 -0
  102. package/dist/core/page/time-format.d.ts +27 -0
  103. package/dist/core/page/time-format.js +84 -0
  104. package/dist/core/page/typed.d.ts +165 -159
  105. package/dist/core/page/workspace.d.ts +25 -1
  106. package/dist/core/page/workspace.js +48 -13
  107. package/dist/core/party/schema.d.ts +74 -3
  108. package/dist/core/party/schema.js +39 -6
  109. package/dist/core/party/types.d.ts +8 -13
  110. package/dist/core/sequence/define.d.ts +1 -2
  111. package/dist/core/slot/company-metadata.d.ts +11 -22
  112. package/dist/core/slot/company-metadata.js +12 -39
  113. package/dist/core/slot/compile-schema.js +6 -6
  114. package/dist/core/slot/field.d.ts +1 -2
  115. package/dist/core/slot/field.js +0 -2
  116. package/dist/core/slot/index.d.ts +3 -3
  117. package/dist/core/slot/index.js +3 -3
  118. package/dist/core/slot/naming.d.ts +1 -1
  119. package/dist/core/slot/naming.js +1 -1
  120. package/dist/core/slot/platform-fields.d.ts +4 -4
  121. package/dist/core/slot/platform-fields.js +5 -5
  122. package/dist/core/slot/schema-compat.d.ts +8 -7
  123. package/dist/core/slot/schema-compat.js +22 -31
  124. package/dist/core/slot/schema.d.ts +3 -59
  125. package/dist/core/slot/schema.js +0 -15
  126. package/dist/core/slot/types.d.ts +4 -10
  127. package/dist/core/slot/value.d.ts +2 -0
  128. package/dist/core/slot/value.js +6 -2
  129. package/dist/dev-overlay/protocol.d.ts +38 -5
  130. package/dist/fn-context/build-context.js +10 -4
  131. package/dist/fn-context/fake/index.d.ts +2 -0
  132. package/dist/fn-context/fake/index.js +5 -3
  133. package/dist/fn-context/fn-params.d.ts +2 -5
  134. package/dist/fn-context/fn-params.js +6 -9
  135. package/dist/fn-context/index.d.ts +1 -1
  136. package/dist/fn-context/make-ctx.js +5 -4
  137. package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +11 -3
  138. package/dist/fn-context/rpc/case-orchestrator-tools.js +0 -1
  139. package/dist/fn-context/rpc/errors.d.ts +13 -0
  140. package/dist/fn-context/rpc/errors.js +22 -0
  141. package/dist/fn-context/rpc/index.d.ts +1 -1
  142. package/dist/fn-context/rpc/index.js +1 -1
  143. package/dist/fn-context/rpc/integration/index.d.ts +11 -1
  144. package/dist/fn-context/rpc/log.d.ts +11 -1
  145. package/dist/fn-context/rpc/platform.d.ts +11 -1
  146. package/dist/fn-context/rpc/records.d.ts +13 -6
  147. package/dist/fn-context/rpc/records.js +2 -4
  148. package/dist/fn-context/rpc/trpc-client.d.ts +42 -0
  149. package/dist/fn-context/rpc/trpc-client.js +151 -22
  150. package/dist/fn-context/rpc/trpc-init.d.ts +19 -1
  151. package/dist/fn-context/rpc/trpc-init.js +10 -4
  152. package/dist/fn-context/rpc/trpc-router-factory.d.ts +66 -8
  153. package/dist/fn-context/run-scope.js +1 -1
  154. package/dist/fn-context/types.d.ts +20 -6
  155. package/dist/index.d.ts +11 -12
  156. package/dist/index.js +5 -8
  157. package/dist/logger.d.ts +4 -0
  158. package/dist/logger.js +8 -0
  159. package/dist/platform.d.ts +32 -33
  160. package/dist/platform.js +23 -22
  161. package/dist/testing-compile-child.d.ts +2 -0
  162. package/dist/testing-compile-child.js +20 -0
  163. package/dist/testing.d.ts +368 -0
  164. package/dist/testing.js +439 -0
  165. package/dist/voice-service/actions.d.ts +12 -35
  166. package/dist/voice-service/index.d.ts +2 -0
  167. package/dist/voice-service/index.js +2 -0
  168. package/dist/voice-service/protocol.d.ts +604 -0
  169. package/dist/voice-service/protocol.js +67 -0
  170. package/dist/voice-service/types.d.ts +16 -6
  171. package/dist/worker.d.ts +2 -1
  172. package/dist/worker.js +2 -1
  173. package/package.json +20 -16
  174. package/dist/core/case-orchestrator/follow-ups.d.ts +0 -31
  175. package/dist/core/case-orchestrator/follow-ups.js +0 -103
  176. package/dist/core/case-orchestrator/tools.d.ts +0 -29
  177. package/dist/core/case-orchestrator/tools.js +0 -31
  178. package/dist/core/case-orchestrator/validate.d.ts +0 -11
  179. package/dist/core/case-orchestrator/validate.js +0 -63
  180. package/dist/core/functions/define/bind.d.ts +0 -25
  181. package/dist/core/functions/define/bind.js +0 -14
  182. package/dist/core/functions/define/case.d.ts +0 -9
  183. package/dist/core/functions/define/case.js +0 -13
  184. package/dist/core/functions/define/events.d.ts +0 -20
  185. package/dist/core/functions/define/events.js +0 -21
  186. package/dist/core/functions/define/no-case.d.ts +0 -4
  187. package/dist/core/functions/define/no-case.js +0 -9
  188. package/dist/core/functions/func-ref.d.ts +0 -25
  189. package/dist/core/functions/func-ref.js +0 -20
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Runs one company script (a page script, or an action's `run`) in a fresh QuickJS context, with
3
+ * async host calls (15 §10.9).
4
+ *
5
+ * The script calls `__host(fn, args)` and gets a promise. Calls made while one slice of script runs
6
+ * are queued; when the slice yields (every pending job done, the script waiting on a host call),
7
+ * the queued calls go to `dispatch` together — this is where `Promise.all` and N+1 reads get merged
8
+ * (15 §10.3). Their results resolve the promises and the next slice runs.
9
+ *
10
+ * Limits: every slice has its own deadline (`sliceMs`; time spent waiting on `dispatch` is not
11
+ * counted against it, nor loading the code before its function is called), the whole run has `totalMs`
12
+ * of wall clock, loading included, plus a memory limit and a cap on host calls.
13
+ */
14
+ export type SandboxLimits = {
15
+ /** CPU budget of one slice: the first run, and each resume after a host call. */
16
+ sliceMs: number;
17
+ /** Wall clock of the whole run, host calls included. */
18
+ totalMs: number;
19
+ memoryBytes: number;
20
+ maxStackBytes: number;
21
+ /** Host calls over the whole run. */
22
+ maxHostCalls: number;
23
+ };
24
+ export declare const DEFAULT_SANDBOX_LIMITS: SandboxLimits;
25
+ /** `console` in the sandbox: its methods, each the company log level of the same name (`log` is `info`). */
26
+ export declare const CONSOLE_METHODS: readonly ['log', 'info', 'warn', 'error', 'debug'];
27
+ export type ConsoleMethod = (typeof CONSOLE_METHODS)[number];
28
+ export type ConsoleLevel = 'debug' | 'info' | 'warn' | 'error';
29
+ /** Where a run's `console` lines go: the company's logs, by the caller (it knows the sequence and the case). */
30
+ export type ConsoleSink = (level: ConsoleLevel, text: string) => void;
31
+ /** One `console` line keeps this many characters; the rest is cut and counted. */
32
+ export declare const CONSOLE_LINE_MAX_CHARS = 2000;
33
+ /** A run (a render, a start, an action) writes this many `console` lines; the rest are counted, then said in one line. */
34
+ export declare const CONSOLE_LINES_PER_RUN = 50;
35
+ export type HostCall = {
36
+ fn: string;
37
+ args: unknown[];
38
+ };
39
+ /** A host call's answer; a refused one may say why in `code` (a read: `invalid_read` / `limit_exceeded`). */
40
+ export type HostResult = {
41
+ ok: true;
42
+ value: unknown;
43
+ } | {
44
+ ok: false;
45
+ error: string;
46
+ code?: string;
47
+ };
48
+ export type SandboxInput = {
49
+ /** A JS expression evaluating to the entry function: `(arg) => value | Promise<value>`. */
50
+ code: string;
51
+ /** Code run before `code`, in the same context (e.g. the page module's helpers). */
52
+ prelude?: string;
53
+ /** JSON-serialisable argument passed to the entry function. */
54
+ arg: unknown;
55
+ /** Runs the calls one slice queued; returns one result per call, in order. */
56
+ dispatch: (calls: HostCall[]) => Promise<HostResult[]>;
57
+ limits?: Partial<SandboxLimits>;
58
+ /** Label for error messages. */
59
+ label?: string;
60
+ /** The name the script's stack frames give `code` (`fn/<sequence>/contextPage.js`, its `functionKey`): what its source map is found by. */
61
+ file?: string;
62
+ /** Where the script's `console` lines go; without it they go nowhere (the script still has `console`). */
63
+ console?: ConsoleSink;
64
+ };
65
+ export type SandboxStats = {
66
+ slices: number;
67
+ hostCalls: number;
68
+ hostBatches: number;
69
+ elapsedMs: number;
70
+ };
71
+ export type SandboxOutput = {
72
+ value: unknown;
73
+ stats: SandboxStats;
74
+ };
75
+ export type SandboxErrorKind = 'compile' | 'threw' | 'slice-timeout' | 'total-timeout' | 'memory' | 'host-calls' | 'stalled' | 'output';
76
+ export declare class SandboxError extends Error {
77
+ readonly kind: SandboxErrorKind;
78
+ /** Where in the script it was thrown (25 §11): the script's own stack, kept for the logs and page tests. */
79
+ readonly scriptStack: string | null;
80
+ constructor(kind: SandboxErrorKind, message: string,
81
+ /** Where in the script it was thrown (25 §11): the script's own stack, kept for the logs and page tests. */
82
+ scriptStack?: string | null);
83
+ }
84
+ export declare function runInSandbox(input: SandboxInput): Promise<SandboxOutput>;
85
+ //# sourceMappingURL=sandbox.d.ts.map
@@ -0,0 +1,357 @@
1
+ import { getQuickJS } from 'quickjs-emscripten';
2
+ export const DEFAULT_SANDBOX_LIMITS = {
3
+ sliceMs: 50,
4
+ totalMs: 2_000,
5
+ memoryBytes: 16 * 1024 * 1024,
6
+ maxStackBytes: 256 * 1024,
7
+ maxHostCalls: 200,
8
+ };
9
+ /** `console` in the sandbox: its methods, each the company log level of the same name (`log` is `info`). */
10
+ export const CONSOLE_METHODS = ['log', 'info', 'warn', 'error', 'debug'];
11
+ /** One `console` line keeps this many characters; the rest is cut and counted. */
12
+ export const CONSOLE_LINE_MAX_CHARS = 2_000;
13
+ /** A run (a render, a start, an action) writes this many `console` lines; the rest are counted, then said in one line. */
14
+ export const CONSOLE_LINES_PER_RUN = 50;
15
+ export class SandboxError extends Error {
16
+ kind;
17
+ scriptStack;
18
+ constructor(kind, message,
19
+ /** Where in the script it was thrown (25 §11): the script's own stack, kept for the logs and page tests. */
20
+ scriptStack = null) {
21
+ super(message);
22
+ this.kind = kind;
23
+ this.scriptStack = scriptStack;
24
+ this.name = 'SandboxError';
25
+ if (scriptStack)
26
+ this.stack = `${this.name}: ${message}\n${scriptStack}`;
27
+ }
28
+ }
29
+ /**
30
+ * A process's first run in QuickJS compiles the engine's code as it goes (about 20ms on an idle machine,
31
+ * several times that on a busy one), which would count against that run's first slice and could fail a
32
+ * script well within its limits. So the first run waits for one untimed run of the same paths: entry,
33
+ * host call, console, JSON.
34
+ */
35
+ let warmedUp = null;
36
+ function warmUp() {
37
+ warmedUp ??= runScript({
38
+ code: `async (arg) => { const got = await __host('warm-up', [arg]); console.log('warm-up'); return JSON.parse(JSON.stringify({ got, list: [1, 'a', null] })); }`,
39
+ arg: { n: 1 },
40
+ dispatch: async (calls) => calls.map(() => ({ ok: true, value: 1 })),
41
+ limits: { sliceMs: 60_000, totalMs: 60_000 },
42
+ label: 'warm-up',
43
+ }).then(() => undefined, () => undefined);
44
+ return warmedUp;
45
+ }
46
+ export async function runInSandbox(input) {
47
+ await warmUp();
48
+ return runScript(input);
49
+ }
50
+ async function runScript(input) {
51
+ const limits = { ...DEFAULT_SANDBOX_LIMITS, ...input.limits };
52
+ const label = input.label ?? 'script';
53
+ const QuickJS = await getQuickJS();
54
+ const startedAt = Date.now();
55
+ const totalDeadline = startedAt + limits.totalMs;
56
+ let sliceDeadline = 0;
57
+ let interruptedBy = null;
58
+ const runtime = QuickJS.newRuntime();
59
+ runtime.setMemoryLimit(limits.memoryBytes);
60
+ runtime.setMaxStackSize(limits.maxStackBytes);
61
+ runtime.setInterruptHandler(() => {
62
+ const now = Date.now();
63
+ if (now > totalDeadline)
64
+ interruptedBy = 'total-timeout';
65
+ else if (now > sliceDeadline)
66
+ interruptedBy = 'slice-timeout';
67
+ else
68
+ return false;
69
+ return true;
70
+ });
71
+ const ctx = runtime.newContext();
72
+ const stats = { slices: 0, hostCalls: 0, hostBatches: 0, elapsedMs: 0 };
73
+ const queued = [];
74
+ let hostCallError = null;
75
+ const consoleLines = consoleWriter(input.console);
76
+ const startSlice = () => {
77
+ sliceDeadline = Date.now() + limits.sliceMs;
78
+ interruptedBy = null;
79
+ stats.slices += 1;
80
+ };
81
+ /** Turns a QuickJS error into a SandboxError, preferring what the interrupt handler saw. */
82
+ const fail = (fallback, errorHandle) => {
83
+ const dumped = ctx.dump(errorHandle);
84
+ errorHandle.dispose();
85
+ const message = errorMessage(dumped);
86
+ if (interruptedBy) {
87
+ return new SandboxError(interruptedBy, interruptedBy === 'slice-timeout'
88
+ ? `${label}: one slice ran over ${limits.sliceMs}ms`
89
+ : `${label}: ran over ${limits.totalMs}ms`);
90
+ }
91
+ if (/out of memory|stack overflow/i.test(message))
92
+ return new SandboxError('memory', `${label}: ${message}`);
93
+ return new SandboxError(fallback, `${label}: ${message}`, scriptStackOf(dumped));
94
+ };
95
+ try {
96
+ // `__host(fn, argsJson)`: queues a host call, returns a promise of its result. Args cross as
97
+ // JSON so nothing but plain data leaves the sandbox.
98
+ const hostFn = ctx.newFunction('__host', (fnHandle, argsHandle) => {
99
+ if (stats.hostCalls >= limits.maxHostCalls) {
100
+ hostCallError = new SandboxError('host-calls', `${label}: more than ${limits.maxHostCalls} host calls`);
101
+ return { error: ctx.newError(hostCallError.message) };
102
+ }
103
+ stats.hostCalls += 1;
104
+ const fn = ctx.getString(fnHandle);
105
+ const args = JSON.parse(ctx.getString(argsHandle));
106
+ const deferred = ctx.newPromise();
107
+ queued.push({ call: { fn, args }, deferred });
108
+ // The VM takes ownership of the returned handle; the deferred keeps its own.
109
+ return deferred.handle.dup();
110
+ });
111
+ ctx.setProp(ctx.global, '__hostRaw', hostFn);
112
+ hostFn.dispose();
113
+ // `__consoleRaw(method, text)`: one console line, written now (not a host call: no promise, no limit).
114
+ const consoleFn = ctx.newFunction('__consoleRaw', (methodHandle, textHandle) => {
115
+ consoleLines.write(ctx.getString(methodHandle), ctx.getString(textHandle));
116
+ });
117
+ ctx.setProp(ctx.global, '__consoleRaw', consoleFn);
118
+ consoleFn.dispose();
119
+ // Loading the code (the bootstrap, the prelude, the bundle's top level) is held to the total only: on a
120
+ // cold process parsing a page bundle alone can take longer than a slice. The script's slices start
121
+ // when its function is called.
122
+ sliceDeadline = totalDeadline;
123
+ interruptedBy = null;
124
+ const bootstrap = ctx.evalCode(`globalThis.__host = (fn, args) =>
125
+ __hostRaw(fn, JSON.stringify(args === undefined ? [] : args)).then((s) => JSON.parse(s));
126
+ ${CONSOLE_BOOTSTRAP}
127
+ ${input.prelude ?? ''}`);
128
+ if (bootstrap.error)
129
+ throw fail('compile', bootstrap.error);
130
+ bootstrap.value.dispose();
131
+ // Wrapped on one line: the compiled code starts on its second line, so no frame of it shifts.
132
+ const entryEval = input.file ? ctx.evalCode(`(${input.code})`, input.file) : ctx.evalCode(`(${input.code})`);
133
+ if (entryEval.error)
134
+ throw fail('compile', entryEval.error);
135
+ const entry = entryEval.value;
136
+ let resultHandle;
137
+ try {
138
+ const argEval = ctx.evalCode(`(${JSON.stringify(input.arg ?? null)})`);
139
+ if (argEval.error)
140
+ throw fail('compile', argEval.error);
141
+ const argHandle = argEval.value;
142
+ try {
143
+ startSlice();
144
+ const called = ctx.callFunction(entry, ctx.undefined, argHandle);
145
+ if (called.error)
146
+ throw fail('threw', called.error);
147
+ resultHandle = called.value;
148
+ }
149
+ finally {
150
+ argHandle.dispose();
151
+ }
152
+ }
153
+ finally {
154
+ entry.dispose();
155
+ }
156
+ let settled;
157
+ try {
158
+ settled = await settle(ctx, runtime, resultHandle, {
159
+ startSlice,
160
+ fail,
161
+ queued,
162
+ dispatch: input.dispatch,
163
+ stats,
164
+ totalDeadline,
165
+ label,
166
+ limits,
167
+ hostCallError: () => hostCallError,
168
+ });
169
+ }
170
+ catch (error) {
171
+ if (resultHandle.alive)
172
+ resultHandle.dispose();
173
+ throw error;
174
+ }
175
+ try {
176
+ startSlice();
177
+ return { value: toJson(ctx, settled, fail), stats: { ...stats, elapsedMs: Date.now() - startedAt } };
178
+ }
179
+ finally {
180
+ settled.dispose();
181
+ }
182
+ }
183
+ finally {
184
+ consoleLines.end();
185
+ for (const { deferred } of queued)
186
+ deferred.dispose();
187
+ ctx.dispose();
188
+ runtime.dispose();
189
+ }
190
+ }
191
+ /**
192
+ * The sandbox's `console`, as Node and the editor have it: each argument a string as it is, an error as
193
+ * `name: message`, anything else as JSON (`String` of it when JSON has none: `undefined`, a function, a
194
+ * BigInt, a cycle), joined by spaces.
195
+ */
196
+ const CONSOLE_BOOTSTRAP = `globalThis.console = (() => {
197
+ const text = (value) => {
198
+ if (typeof value === 'string') return value;
199
+ if (value instanceof Error) return value.name + ': ' + value.message;
200
+ try {
201
+ const json = JSON.stringify(value);
202
+ if (json !== undefined) return json;
203
+ } catch {}
204
+ return String(value);
205
+ };
206
+ const raw = __consoleRaw;
207
+ const out = {};
208
+ for (const method of ${JSON.stringify(CONSOLE_METHODS)}) out[method] = (...args) => { raw(method, args.map(text).join(' ')); };
209
+ return out;
210
+ })();`;
211
+ const CONSOLE_LEVELS = { log: 'info', info: 'info', warn: 'warn', error: 'error', debug: 'debug' };
212
+ /**
213
+ * A run's `console` lines into its sink: each cut at {@link CONSOLE_LINE_MAX_CHARS}, at most
214
+ * {@link CONSOLE_LINES_PER_RUN} of them, and at the end one line saying how many more were dropped.
215
+ */
216
+ function consoleWriter(sink) {
217
+ let written = 0;
218
+ let dropped = 0;
219
+ // A log line that cannot be written never fails the script.
220
+ const emit = (level, text) => {
221
+ try {
222
+ sink?.(level, text);
223
+ }
224
+ catch {
225
+ // dropped
226
+ }
227
+ };
228
+ return {
229
+ write(method, text) {
230
+ if (!sink)
231
+ return;
232
+ if (written >= CONSOLE_LINES_PER_RUN) {
233
+ dropped += 1;
234
+ return;
235
+ }
236
+ written += 1;
237
+ const line = text.length > CONSOLE_LINE_MAX_CHARS
238
+ ? `${text.slice(0, CONSOLE_LINE_MAX_CHARS)}… (${text.length - CONSOLE_LINE_MAX_CHARS} more characters)`
239
+ : text;
240
+ emit(CONSOLE_LEVELS[method] ?? 'info', line);
241
+ },
242
+ end() {
243
+ if (dropped > 0) {
244
+ emit('warn', `${dropped} more console ${dropped === 1 ? 'line was' : 'lines were'} dropped: a run writes at most ${CONSOLE_LINES_PER_RUN}`);
245
+ }
246
+ },
247
+ };
248
+ }
249
+ /** Pumps jobs and host calls until the entry's result is no longer a pending promise. */
250
+ async function settle(ctx, runtime, handle, env) {
251
+ for (;;) {
252
+ const jobs = runtime.executePendingJobs();
253
+ if (jobs.error)
254
+ throw env.fail('threw', jobs.error);
255
+ const hostError = env.hostCallError();
256
+ if (hostError)
257
+ throw hostError;
258
+ const state = ctx.getPromiseState(handle);
259
+ if (state.type === 'fulfilled') {
260
+ if (state.notAPromise)
261
+ return handle;
262
+ handle.dispose();
263
+ return state.value;
264
+ }
265
+ if (state.type === 'rejected') {
266
+ handle.dispose();
267
+ throw env.fail('threw', state.error);
268
+ }
269
+ const batch = env.queued.splice(0);
270
+ if (batch.length === 0) {
271
+ throw new SandboxError('stalled', `${env.label}: waiting on a promise that no host call will settle`);
272
+ }
273
+ const remaining = env.totalDeadline - Date.now();
274
+ if (remaining <= 0) {
275
+ for (const { deferred } of batch)
276
+ deferred.dispose();
277
+ throw new SandboxError('total-timeout', `${env.label}: ran over ${env.limits.totalMs}ms`);
278
+ }
279
+ env.stats.hostBatches += 1;
280
+ let results;
281
+ try {
282
+ results = await withTimeout(env.dispatch(batch.map((q) => q.call)), remaining, () => new SandboxError('total-timeout', `${env.label}: ran over ${env.limits.totalMs}ms`));
283
+ }
284
+ catch (error) {
285
+ for (const { deferred } of batch)
286
+ deferred.dispose();
287
+ throw error;
288
+ }
289
+ if (results.length !== batch.length) {
290
+ for (const { deferred } of batch)
291
+ deferred.dispose();
292
+ throw new Error(`${env.label}: dispatch returned ${results.length} results for ${batch.length} calls`);
293
+ }
294
+ env.startSlice();
295
+ batch.forEach(({ deferred }, i) => {
296
+ const result = results[i];
297
+ if (result.ok) {
298
+ const json = ctx.newString(JSON.stringify(result.value ?? null));
299
+ deferred.resolve(json);
300
+ json.dispose();
301
+ }
302
+ else {
303
+ const error = ctx.newError(result.error);
304
+ deferred.reject(error);
305
+ error.dispose();
306
+ }
307
+ deferred.dispose();
308
+ });
309
+ }
310
+ }
311
+ /** The result as plain JSON, serialised inside the VM so cycles and BigInts fail loudly. */
312
+ function toJson(ctx, handle, fail) {
313
+ const stringify = ctx.evalCode('(v) => v === undefined ? "null" : JSON.stringify(v)');
314
+ if (stringify.error)
315
+ throw fail('output', stringify.error);
316
+ try {
317
+ const out = ctx.callFunction(stringify.value, ctx.undefined, handle);
318
+ if (out.error)
319
+ throw fail('output', out.error);
320
+ try {
321
+ return JSON.parse(ctx.getString(out.value));
322
+ }
323
+ finally {
324
+ out.value.dispose();
325
+ }
326
+ }
327
+ finally {
328
+ stringify.value.dispose();
329
+ }
330
+ }
331
+ async function withTimeout(promise, ms, onTimeout) {
332
+ let timer;
333
+ try {
334
+ return await Promise.race([
335
+ promise,
336
+ new Promise((_, reject) => {
337
+ timer = setTimeout(() => reject(onTimeout()), ms);
338
+ }),
339
+ ]);
340
+ }
341
+ finally {
342
+ clearTimeout(timer);
343
+ }
344
+ }
345
+ /** The script's stack of a thrown error, as QuickJS dumps it; null when it has none. */
346
+ function scriptStackOf(dumped) {
347
+ const stack = dumped && typeof dumped === 'object' ? dumped.stack : undefined;
348
+ return typeof stack === 'string' && stack.trim() ? stack.trimEnd() : null;
349
+ }
350
+ function errorMessage(dumped) {
351
+ if (dumped && typeof dumped === 'object' && 'message' in dumped) {
352
+ const { name, message } = dumped;
353
+ return name && name !== 'Error' ? `${String(name)}: ${String(message)}` : String(message);
354
+ }
355
+ return String(dumped);
356
+ }
357
+ //# sourceMappingURL=sandbox.js.map
@@ -16,4 +16,31 @@ export declare function formatTime(at: string | number, options: FormatTimeOptio
16
16
  * key a rule by date. Throws on a time or zone it cannot read.
17
17
  */
18
18
  export declare function dayIn(at: string | number | Date, timeZone: string): string;
19
+ /**
20
+ * A moment as it reads on a clock in a time zone (74 §4.7): `ctx.time.parts`. `weekday` is ISO: 1 Monday …
21
+ * 7 Sunday; `offset` the zone's offset from UTC then (`-05:00`).
22
+ */
23
+ export type TimeParts = {
24
+ year: number;
25
+ month: number;
26
+ day: number;
27
+ hour: number;
28
+ minute: number;
29
+ second: number;
30
+ weekday: 1 | 2 | 3 | 4 | 5 | 6 | 7;
31
+ /** `YYYY-MM-DD`. */
32
+ date: string;
33
+ /** `HH:mm`, 24-hour. */
34
+ time: string;
35
+ offset: string;
36
+ };
37
+ /** `at` on a clock in `timeZone`: `ctx.time.parts` (74 §4.7). Throws on a time or zone it cannot read. */
38
+ export declare function timeParts(at: string | number | Date, timeZone: string): TimeParts;
39
+ /**
40
+ * The exact moment (ISO 8601, UTC) a clock in `timeZone` shows `date` (`YYYY-MM-DD`) at `time` (`HH:mm` or
41
+ * `HH:mm:ss`): `ctx.time.at` (74 §4.7), "the day before delivery at 17:00 in the customer's zone". A time the
42
+ * clocks skip (spring forward) is the moment just after the gap, as the clock shows it then; a time they show
43
+ * twice (fall back), the first. Throws on a date, time or zone it cannot read.
44
+ */
45
+ export declare function timeAt(date: string, time: string, timeZone: string): string;
19
46
  //# sourceMappingURL=time-format.d.ts.map
@@ -38,4 +38,88 @@ export function dayIn(at, timeZone) {
38
38
  throw new Error(`time.day: ${timeZone} is not a time zone`);
39
39
  }
40
40
  }
41
+ const WEEKDAYS = { Mon: 1, Tue: 2, Wed: 3, Thu: 4, Fri: 5, Sat: 6, Sun: 7 };
42
+ function clockIn(ms, timeZone, what) {
43
+ let parts;
44
+ try {
45
+ parts = new Intl.DateTimeFormat('en-US', {
46
+ timeZone,
47
+ hourCycle: 'h23',
48
+ year: 'numeric',
49
+ month: 'numeric',
50
+ day: 'numeric',
51
+ hour: 'numeric',
52
+ minute: 'numeric',
53
+ second: 'numeric',
54
+ weekday: 'short',
55
+ }).formatToParts(new Date(ms));
56
+ }
57
+ catch {
58
+ throw new Error(`${what}: ${timeZone} is not a time zone`);
59
+ }
60
+ const part = (type) => parts.find((p) => p.type === type)?.value ?? '';
61
+ return {
62
+ year: Number(part('year')),
63
+ month: Number(part('month')),
64
+ day: Number(part('day')),
65
+ hour: Number(part('hour')),
66
+ minute: Number(part('minute')),
67
+ second: Number(part('second')),
68
+ weekday: WEEKDAYS[part('weekday')] ?? 1,
69
+ };
70
+ }
71
+ /** The zone's offset from UTC at `ms`, in minutes (New York in winter: -300). */
72
+ function offsetMinutes(ms, timeZone, what) {
73
+ const c = clockIn(ms, timeZone, what);
74
+ const asUtc = Date.UTC(c.year, c.month - 1, c.day, c.hour, c.minute, c.second);
75
+ return Math.round((asUtc - Math.floor(ms / 1000) * 1000) / 60_000);
76
+ }
77
+ const pad = (n, width = 2) => String(Math.abs(n)).padStart(width, '0');
78
+ function offsetText(minutes) {
79
+ return `${minutes < 0 ? '-' : '+'}${pad(Math.floor(Math.abs(minutes) / 60))}:${pad(Math.abs(minutes) % 60)}`;
80
+ }
81
+ /** `at` on a clock in `timeZone`: `ctx.time.parts` (74 §4.7). Throws on a time or zone it cannot read. */
82
+ export function timeParts(at, timeZone) {
83
+ const ms = new Date(at).getTime();
84
+ if (Number.isNaN(ms))
85
+ throw new Error(`time.parts: ${String(at)} is not a time`);
86
+ const c = clockIn(ms, timeZone, 'time.parts');
87
+ return {
88
+ ...c,
89
+ date: `${pad(c.year, 4)}-${pad(c.month)}-${pad(c.day)}`,
90
+ time: `${pad(c.hour)}:${pad(c.minute)}`,
91
+ offset: offsetText(offsetMinutes(ms, timeZone, 'time.parts')),
92
+ };
93
+ }
94
+ /**
95
+ * The exact moment (ISO 8601, UTC) a clock in `timeZone` shows `date` (`YYYY-MM-DD`) at `time` (`HH:mm` or
96
+ * `HH:mm:ss`): `ctx.time.at` (74 §4.7), "the day before delivery at 17:00 in the customer's zone". A time the
97
+ * clocks skip (spring forward) is the moment just after the gap, as the clock shows it then; a time they show
98
+ * twice (fall back), the first. Throws on a date, time or zone it cannot read.
99
+ */
100
+ export function timeAt(date, time, timeZone) {
101
+ const d = /^(\d{4})-(\d{2})-(\d{2})$/.exec(date);
102
+ const t = /^(\d{2}):(\d{2})(?::(\d{2}))?$/.exec(time);
103
+ if (!d)
104
+ throw new Error(`time.at: ${date} is not a date (YYYY-MM-DD)`);
105
+ if (!t)
106
+ throw new Error(`time.at: ${time} is not a time (HH:mm)`);
107
+ const [year, month, day] = [Number(d[1]), Number(d[2]), Number(d[3])];
108
+ const [hour, minute, second] = [Number(t[1]), Number(t[2]), Number(t[3] ?? 0)];
109
+ const wall = Date.UTC(year, month - 1, day, hour, minute, second);
110
+ const check = new Date(wall);
111
+ if (check.getUTCMonth() !== month - 1 || check.getUTCDate() !== day || hour > 23 || minute > 59 || second > 59) {
112
+ throw new Error(`time.at: ${date} ${time} is no date and time`);
113
+ }
114
+ // The offsets on either side of the moment: the earlier one first, so a time shown twice is its first.
115
+ const before = offsetMinutes(wall - 24 * 3_600_000, timeZone, 'time.at');
116
+ const after = offsetMinutes(wall + 24 * 3_600_000, timeZone, 'time.at');
117
+ for (const offset of [before, after]) {
118
+ const ms = wall - offset * 60_000;
119
+ if (offsetMinutes(ms, timeZone, 'time.at') === offset)
120
+ return new Date(ms).toISOString();
121
+ }
122
+ // In the gap the clocks skip: read with the offset before it, it lands as far past the gap.
123
+ return new Date(wall - before * 60_000).toISOString();
124
+ }
41
125
  //# sourceMappingURL=time-format.js.map