@superpanel-sdk/runtime 0.2.3 → 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.
- package/README.md +30 -10
- package/dist/builder/codegen/emit.d.ts +25 -12
- package/dist/builder/codegen/emit.js +339 -78
- package/dist/builder/codegen/generate.d.ts +21 -2
- package/dist/builder/codegen/generate.js +147 -34
- package/dist/builder/company-import-graph.d.ts +2 -0
- package/dist/builder/company-import-graph.js +40 -8
- package/dist/builder/company-imports.d.ts +29 -0
- package/dist/builder/company-imports.js +122 -0
- package/dist/builder/compile/compile-sequence.d.ts +61 -8
- package/dist/builder/compile/compile-sequence.js +334 -80
- package/dist/builder/compile/function-code.d.ts +19 -3
- package/dist/builder/compile/function-code.js +123 -45
- package/dist/builder/compile/registry.d.ts +7 -3
- package/dist/builder/compile/registry.js +30 -27
- package/dist/builder/errors.d.ts +9 -0
- package/dist/builder/errors.js +17 -0
- package/dist/builder/function-params.d.ts +10 -0
- package/dist/builder/function-params.js +203 -0
- package/dist/builder/load.d.ts +70 -10
- package/dist/builder/load.js +258 -67
- package/dist/builder/paths.d.ts +9 -1
- package/dist/builder/paths.js +32 -2
- package/dist/builder/schedules.d.ts +25 -0
- package/dist/builder/schedules.js +101 -0
- package/dist/builder/slot-schema-validation.d.ts +4 -0
- package/dist/builder/slot-schema-validation.js +10 -0
- package/dist/builder/typecheck.d.ts +8 -0
- package/dist/builder/typecheck.js +102 -0
- package/dist/builder/types.d.ts +127 -19
- package/dist/builder/types.js +21 -1
- package/dist/cli/auth/auth.d.ts +37 -0
- package/dist/cli/auth/auth.js +211 -0
- package/dist/cli/auth/backend-url.d.ts +5 -0
- package/dist/cli/auth/backend-url.js +25 -0
- package/dist/cli/auth/clipboard.d.ts +9 -0
- package/dist/cli/auth/clipboard.js +38 -0
- package/dist/cli/auth/http.d.ts +20 -0
- package/dist/cli/auth/http.js +27 -0
- package/dist/cli/auth/index.d.ts +10 -0
- package/dist/cli/auth/index.js +27 -0
- package/dist/cli/auth/progress.d.ts +12 -0
- package/dist/cli/auth/progress.js +15 -0
- package/dist/cli/auth/serve-login.d.ts +8 -0
- package/dist/cli/auth/serve-login.js +24 -0
- package/dist/cli/calls/index.d.ts +177 -0
- package/dist/cli/calls/index.js +96 -0
- package/dist/cli/compile/index.js +3 -0
- package/dist/cli/config.d.ts +10 -0
- package/dist/cli/config.js +52 -0
- package/dist/cli/debug/index.d.ts +31 -0
- package/dist/cli/debug/index.js +119 -0
- package/dist/cli/exec/index.d.ts +39 -0
- package/dist/cli/exec/index.js +138 -0
- package/dist/cli/exec/session.d.ts +48 -0
- package/dist/cli/exec/session.js +82 -0
- package/dist/cli/exec/trace.d.ts +18 -0
- package/dist/cli/exec/trace.js +52 -0
- package/dist/cli/index.js +112 -9
- package/dist/cli/logs/index.d.ts +47 -0
- package/dist/cli/logs/index.js +113 -0
- package/dist/cli/mobile/index.d.ts +12 -0
- package/dist/cli/mobile/index.js +405 -0
- package/dist/cli/serve/index.d.ts +109 -3
- package/dist/cli/serve/index.js +506 -118
- package/dist/cli/turns/index.d.ts +105 -0
- package/dist/cli/turns/index.js +128 -0
- package/dist/cli/turns/run.d.ts +13 -0
- package/dist/cli/turns/run.js +57 -0
- package/dist/cli/turns/shadow.d.ts +112 -0
- package/dist/cli/turns/shadow.js +54 -0
- package/dist/cli/type-gen/index.js +9 -9
- package/dist/compiler.d.ts +7 -0
- package/dist/compiler.js +7 -0
- package/dist/compute-host/compute-host.d.ts +14 -23
- package/dist/compute-host/compute-host.js +18 -30
- package/dist/compute-host/index.d.ts +2 -4
- package/dist/compute-host/index.js +1 -2
- package/dist/compute-host/source-compute-host.d.ts +10 -10
- package/dist/compute-host/source-compute-host.js +33 -40
- package/dist/core/case-orchestrator/contact-hours.d.ts +70 -0
- package/dist/core/case-orchestrator/contact-hours.js +127 -0
- package/dist/core/case-orchestrator/index.d.ts +3 -0
- package/dist/core/case-orchestrator/index.js +3 -0
- package/dist/core/case-orchestrator/render.d.ts +75 -0
- package/dist/core/case-orchestrator/render.js +244 -0
- package/dist/core/case-orchestrator/time.d.ts +11 -0
- package/dist/core/case-orchestrator/time.js +29 -0
- package/dist/core/case-orchestrator/types.d.ts +708 -0
- package/dist/core/case-orchestrator/types.js +42 -0
- package/dist/core/context/call.d.ts +291 -0
- package/dist/core/context/call.js +94 -0
- package/dist/core/context/channels.d.ts +60 -0
- package/dist/core/context/channels.js +35 -0
- package/dist/core/context/contact.d.ts +96 -6
- package/dist/core/context/contact.js +48 -2
- package/dist/core/context/email-message.d.ts +198 -0
- package/dist/core/context/email-message.js +86 -0
- package/dist/core/context/email.d.ts +109 -0
- package/dist/core/context/email.js +90 -0
- package/dist/core/context/exchanges.d.ts +167 -0
- package/dist/core/context/exchanges.js +81 -0
- package/dist/core/context/instances.d.ts +307 -0
- package/dist/core/context/instances.js +137 -0
- package/dist/core/context/kv.d.ts +113 -0
- package/dist/core/context/kv.js +60 -0
- package/dist/core/context/platform-registry.d.ts +544 -4
- package/dist/core/context/platform-registry.js +14 -2
- package/dist/core/context/platform.d.ts +2 -1
- package/dist/core/context/sms.d.ts +177 -0
- package/dist/core/context/sms.js +77 -0
- package/dist/core/context/types.d.ts +79 -48
- package/dist/core/context/types.js +1 -7
- package/dist/core/conversation/index.d.ts +85 -0
- package/dist/core/conversation/index.js +133 -0
- package/dist/core/functions/errors.d.ts +74 -0
- package/dist/core/functions/errors.js +26 -0
- package/dist/core/functions/events.d.ts +192 -0
- package/dist/core/functions/events.js +23 -0
- package/dist/core/functions/schedules.d.ts +50 -0
- package/dist/core/functions/schedules.js +20 -0
- package/dist/core/functions/types.d.ts +29 -73
- package/dist/core/functions/types.js +1 -4
- package/dist/core/office-hours/index.d.ts +39 -0
- package/dist/core/office-hours/index.js +187 -0
- package/dist/core/page/define.d.ts +904 -0
- package/dist/core/page/define.js +518 -0
- package/dist/core/page/envelope.d.ts +55 -0
- package/dist/core/page/envelope.js +4 -0
- package/dist/core/page/fns.d.ts +448 -0
- package/dist/core/page/fns.js +539 -0
- package/dist/core/page/memory-reader.d.ts +27 -0
- package/dist/core/page/memory-reader.js +299 -0
- package/dist/core/page/ops.d.ts +58 -0
- package/dist/core/page/ops.js +220 -0
- package/dist/core/page/page-host.d.ts +20 -0
- package/dist/core/page/page-host.js +57 -0
- package/dist/core/page/records-query.d.ts +234 -0
- package/dist/core/page/records-query.js +780 -0
- package/dist/core/page/render.d.ts +295 -0
- package/dist/core/page/render.js +1060 -0
- package/dist/core/page/sandbox.d.ts +85 -0
- package/dist/core/page/sandbox.js +357 -0
- package/dist/core/page/time-format.d.ts +46 -0
- package/dist/core/page/time-format.js +125 -0
- package/dist/core/page/timeline.d.ts +40 -0
- package/dist/core/page/timeline.js +2 -0
- package/dist/core/page/typed.d.ts +648 -0
- package/dist/core/page/typed.js +2 -0
- package/dist/core/page/workspace.d.ts +57 -0
- package/dist/core/page/workspace.js +266 -0
- package/dist/core/party/index.d.ts +3 -0
- package/dist/core/party/index.js +3 -0
- package/dist/core/party/schema.d.ts +208 -0
- package/dist/core/party/schema.js +113 -0
- package/dist/core/party/types.d.ts +35 -0
- package/dist/core/party/types.js +15 -0
- package/dist/core/sequence/define.d.ts +56 -0
- package/dist/core/sequence/define.js +10 -0
- package/dist/core/sequence/index.d.ts +63 -0
- package/dist/core/sequence/index.js +231 -0
- package/dist/core/slot/binding.d.ts +40 -0
- package/dist/core/slot/binding.js +2 -0
- package/dist/core/slot/company-metadata.d.ts +102 -0
- package/dist/core/slot/company-metadata.js +536 -0
- package/dist/core/slot/compile-schema.d.ts +19 -13
- package/dist/core/slot/compile-schema.js +51 -248
- package/dist/core/slot/contact-timezone.d.ts +110 -0
- package/dist/core/slot/contact-timezone.js +186 -0
- package/dist/core/slot/field.d.ts +2 -25
- package/dist/core/slot/field.js +0 -71
- package/dist/core/slot/id-ref.d.ts +17 -2
- package/dist/core/slot/id-ref.js +16 -8
- package/dist/core/slot/index.d.ts +12 -13
- package/dist/core/slot/index.js +11 -12
- package/dist/core/slot/naming.d.ts +15 -0
- package/dist/core/slot/naming.js +96 -0
- package/dist/core/slot/platform-fields.d.ts +119 -0
- package/dist/core/slot/platform-fields.js +121 -0
- package/dist/core/slot/ref.d.ts +16 -0
- package/dist/core/slot/ref.js +14 -0
- package/dist/core/slot/row.d.ts +2 -9
- package/dist/core/slot/row.js +13 -23
- package/dist/core/slot/schema-compat.d.ts +17 -0
- package/dist/core/slot/schema-compat.js +103 -0
- package/dist/core/slot/schema.d.ts +353 -76
- package/dist/core/slot/schema.js +163 -10
- package/dist/core/slot/types.d.ts +30 -103
- package/dist/core/slot/value.d.ts +28 -3
- package/dist/core/slot/value.js +93 -57
- package/dist/dev-overlay/protocol.d.ts +74 -8
- package/dist/fn-context/build-context.d.ts +3 -1
- package/dist/fn-context/build-context.js +26 -10
- package/dist/fn-context/fake/empty-api.js +9 -28
- package/dist/fn-context/fake/index.d.ts +13 -4
- package/dist/fn-context/fake/index.js +29 -2
- package/dist/fn-context/fn-params.d.ts +3 -0
- package/dist/fn-context/fn-params.js +10 -0
- package/dist/fn-context/index.d.ts +5 -5
- package/dist/fn-context/index.js +4 -4
- package/dist/fn-context/log.js +2 -2
- package/dist/fn-context/make-ctx.d.ts +11 -0
- package/dist/fn-context/make-ctx.js +20 -9
- package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +32 -0
- package/dist/fn-context/rpc/case-orchestrator-tools.js +12 -0
- package/dist/fn-context/rpc/errors.d.ts +13 -0
- package/dist/fn-context/rpc/errors.js +22 -0
- package/dist/fn-context/rpc/index.d.ts +4 -2
- package/dist/fn-context/rpc/index.js +4 -2
- package/dist/fn-context/rpc/integration/index.d.ts +15 -5
- package/dist/fn-context/rpc/integration/index.js +1 -1
- package/dist/fn-context/rpc/integration/list-surfaces.d.ts +21 -0
- package/dist/fn-context/rpc/integration/{list-bindings.js → list-surfaces.js} +4 -4
- package/dist/fn-context/rpc/log.d.ts +11 -1
- package/dist/fn-context/rpc/platform.d.ts +13 -3
- package/dist/fn-context/rpc/platform.js +1 -1
- package/dist/fn-context/rpc/records.d.ts +85 -0
- package/dist/fn-context/rpc/records.js +24 -0
- package/dist/fn-context/rpc/trpc-client.d.ts +44 -2
- package/dist/fn-context/rpc/trpc-client.js +222 -12
- package/dist/fn-context/rpc/trpc-init.d.ts +21 -3
- package/dist/fn-context/rpc/trpc-init.js +10 -4
- package/dist/fn-context/rpc/trpc-router-factory.d.ts +111 -7
- package/dist/fn-context/rpc/trpc-router-factory.js +2 -0
- package/dist/fn-context/run-scope.d.ts +4 -0
- package/dist/fn-context/run-scope.js +9 -0
- package/dist/fn-context/types.d.ts +52 -5
- package/dist/index.d.ts +36 -46
- package/dist/index.js +30 -33
- package/dist/logger.d.ts +11 -0
- package/dist/logger.js +49 -0
- package/dist/platform.d.ts +75 -0
- package/dist/platform.js +57 -0
- package/dist/testing-compile-child.d.ts +2 -0
- package/dist/testing-compile-child.js +20 -0
- package/dist/testing.d.ts +368 -0
- package/dist/testing.js +439 -0
- package/dist/voice-service/actions.d.ts +50 -6
- package/dist/voice-service/index.d.ts +2 -0
- package/dist/voice-service/index.js +2 -0
- package/dist/voice-service/protocol.d.ts +604 -0
- package/dist/voice-service/protocol.js +67 -0
- package/dist/voice-service/types.d.ts +55 -8
- package/dist/worker.d.ts +11 -0
- package/dist/worker.js +11 -0
- package/package.json +38 -15
- package/dist/builder/compile/compile-agenda.d.ts +0 -15
- package/dist/builder/compile/compile-agenda.js +0 -64
- package/dist/builder/merge.d.ts +0 -4
- package/dist/builder/merge.js +0 -19
- package/dist/compute-host/read-only-cache.d.ts +0 -20
- package/dist/compute-host/read-only-cache.js +0 -61
- package/dist/core/context/db.d.ts +0 -69
- package/dist/core/context/db.js +0 -44
- package/dist/core/context/docusign.d.ts +0 -134
- package/dist/core/context/docusign.js +0 -108
- package/dist/core/context/filevine.d.ts +0 -323
- package/dist/core/context/filevine.js +0 -254
- package/dist/core/context/http.d.ts +0 -9
- package/dist/core/context/http.js +0 -4
- package/dist/core/context/hubspot.d.ts +0 -250
- package/dist/core/context/hubspot.js +0 -191
- package/dist/core/context/twilio.d.ts +0 -50
- package/dist/core/context/twilio.js +0 -42
- package/dist/core/functions/define/case-context.d.ts +0 -14
- package/dist/core/functions/define/case-context.js +0 -15
- package/dist/core/functions/define/compute.d.ts +0 -7
- package/dist/core/functions/define/compute.js +0 -17
- package/dist/core/functions/define/custom.d.ts +0 -4
- package/dist/core/functions/define/custom.js +0 -8
- package/dist/core/functions/define/system.d.ts +0 -7
- package/dist/core/functions/define/system.js +0 -12
- package/dist/core/functions/func-ref.d.ts +0 -16
- package/dist/core/functions/func-ref.js +0 -20
- package/dist/core/slot/deps.d.ts +0 -15
- package/dist/core/slot/deps.js +0 -88
- package/dist/core/slot/derived.d.ts +0 -32
- package/dist/core/slot/derived.js +0 -115
- package/dist/core/slot/platform-session-fields.d.ts +0 -38
- package/dist/core/slot/platform-session-fields.js +0 -48
- package/dist/core/slot/sf-schema.d.ts +0 -23
- package/dist/core/slot/sf-schema.js +0 -125
- package/dist/core/slot/sf-validate.d.ts +0 -14
- package/dist/core/slot/sf-validate.js +0 -132
- package/dist/core/slot/snapshot.d.ts +0 -18
- package/dist/core/slot/snapshot.js +0 -138
- package/dist/core/slot/write.d.ts +0 -48
- package/dist/core/slot/write.js +0 -426
- package/dist/core/stage/builders.d.ts +0 -54
- package/dist/core/stage/builders.js +0 -31
- package/dist/core/stage/condition.d.ts +0 -17
- package/dist/core/stage/condition.js +0 -198
- package/dist/core/stage/drive.d.ts +0 -43
- package/dist/core/stage/drive.js +0 -78
- package/dist/core/stage/finish-when.d.ts +0 -20
- package/dist/core/stage/finish-when.js +0 -82
- package/dist/core/stage/flatten.d.ts +0 -27
- package/dist/core/stage/flatten.js +0 -466
- package/dist/core/stage/index.d.ts +0 -15
- package/dist/core/stage/index.js +0 -13
- package/dist/core/stage/interrupts.d.ts +0 -28
- package/dist/core/stage/interrupts.js +0 -120
- package/dist/core/stage/ivr-fields.d.ts +0 -9
- package/dist/core/stage/ivr-fields.js +0 -75
- package/dist/core/stage/simulate.d.ts +0 -24
- package/dist/core/stage/simulate.js +0 -33
- package/dist/core/stage/transfer-fields.d.ts +0 -19
- package/dist/core/stage/transfer-fields.js +0 -129
- package/dist/core/stage/transfer-spec.d.ts +0 -48
- package/dist/core/stage/transfer-spec.js +0 -113
- package/dist/core/stage/types.d.ts +0 -208
- package/dist/core/stage/types.js +0 -2
- package/dist/core/stage/validate.d.ts +0 -11
- package/dist/core/stage/validate.js +0 -330
- package/dist/core/stage/walk.d.ts +0 -39
- package/dist/core/stage/walk.js +0 -119
- package/dist/fn-context/rpc/integration/list-bindings.d.ts +0 -21
- package/dist/fn-context/system-args.d.ts +0 -6
- package/dist/fn-context/system-args.js +0 -14
|
@@ -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
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Times as people read them, in a time zone (25 §11). The page's sandbox has no `Intl` (a date's
|
|
3
|
+
* `toLocaleString` ignores the zone there), so a page formats through the platform (`ctx.time.format`),
|
|
4
|
+
* which runs this; a function, on the Worker, calls it as it is (`ctx.time.format`).
|
|
5
|
+
*/
|
|
6
|
+
/** How much of the time to show: `'datetime'` (default) "Tue, Nov 3, 3:00 PM EST", `'date'` "Tue, Nov 3, 2026", `'time'` "3:00 PM EST". */
|
|
7
|
+
export type TimeStyle = 'datetime' | 'date' | 'time';
|
|
8
|
+
export type FormatTimeOptions = {
|
|
9
|
+
timeZone: string;
|
|
10
|
+
style?: TimeStyle;
|
|
11
|
+
};
|
|
12
|
+
/** `at` (ISO 8601, or epoch ms) in `timeZone`, in US English. Throws on a time or zone it cannot read. */
|
|
13
|
+
export declare function formatTime(at: string | number, options: FormatTimeOptions): string;
|
|
14
|
+
/**
|
|
15
|
+
* The calendar day (`YYYY-MM-DD`) at `at` in `timeZone`: `ctx.time.day` (25 §11), to compare days or
|
|
16
|
+
* key a rule by date. Throws on a time or zone it cannot read.
|
|
17
|
+
*/
|
|
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;
|
|
46
|
+
//# sourceMappingURL=time-format.d.ts.map
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Times as people read them, in a time zone (25 §11). The page's sandbox has no `Intl` (a date's
|
|
3
|
+
* `toLocaleString` ignores the zone there), so a page formats through the platform (`ctx.time.format`),
|
|
4
|
+
* which runs this; a function, on the Worker, calls it as it is (`ctx.time.format`).
|
|
5
|
+
*/
|
|
6
|
+
const STYLES = {
|
|
7
|
+
datetime: { weekday: 'short', month: 'short', day: 'numeric', hour: 'numeric', minute: '2-digit', timeZoneName: 'short' },
|
|
8
|
+
date: { weekday: 'short', month: 'short', day: 'numeric', year: 'numeric' },
|
|
9
|
+
time: { hour: 'numeric', minute: '2-digit', timeZoneName: 'short' },
|
|
10
|
+
};
|
|
11
|
+
/** `at` (ISO 8601, or epoch ms) in `timeZone`, in US English. Throws on a time or zone it cannot read. */
|
|
12
|
+
export function formatTime(at, options) {
|
|
13
|
+
const date = new Date(at);
|
|
14
|
+
if (Number.isNaN(date.getTime()))
|
|
15
|
+
throw new Error(`time.format: ${String(at)} is not a time`);
|
|
16
|
+
const style = options.style ?? 'datetime';
|
|
17
|
+
if (!(style in STYLES))
|
|
18
|
+
throw new Error(`time.format: style is 'datetime', 'date' or 'time'`);
|
|
19
|
+
try {
|
|
20
|
+
return new Intl.DateTimeFormat('en-US', { ...STYLES[style], timeZone: options.timeZone }).format(date);
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
throw new Error(`time.format: ${options.timeZone} is not a time zone`);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* The calendar day (`YYYY-MM-DD`) at `at` in `timeZone`: `ctx.time.day` (25 §11), to compare days or
|
|
28
|
+
* key a rule by date. Throws on a time or zone it cannot read.
|
|
29
|
+
*/
|
|
30
|
+
export function dayIn(at, timeZone) {
|
|
31
|
+
const date = new Date(at);
|
|
32
|
+
if (Number.isNaN(date.getTime()))
|
|
33
|
+
throw new Error(`time.day: ${String(at)} is not a time`);
|
|
34
|
+
try {
|
|
35
|
+
return new Intl.DateTimeFormat('en-CA', { timeZone, year: 'numeric', month: '2-digit', day: '2-digit' }).format(date);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
throw new Error(`time.day: ${timeZone} is not a time zone`);
|
|
39
|
+
}
|
|
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
|
+
}
|
|
125
|
+
//# sourceMappingURL=time-format.js.map
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The case's timeline (20 §3, 21 §11.6): everything that happened on one instance, merged and in time
|
|
3
|
+
* order — messages, calls, the AI's runs and what they did, field changes, scheduled contacts, the
|
|
4
|
+
* Orchestrator. One shape for every source, so a page, the web and a prompt read it the same way.
|
|
5
|
+
*/
|
|
6
|
+
export type TimelineKind = 'message' | 'call' | 'run' | 'action' | 'write' | 'scheduled' | 'orchestrator';
|
|
7
|
+
export declare const TIMELINE_KINDS: readonly TimelineKind[];
|
|
8
|
+
export type TimelineEntry = {
|
|
9
|
+
/** The source row's id. */
|
|
10
|
+
id: string;
|
|
11
|
+
kind: TimelineKind;
|
|
12
|
+
/** ISO time it happened (a message's time, a call's start, a run's start, a write). */
|
|
13
|
+
at: string;
|
|
14
|
+
channel: 'call' | 'sms' | 'email' | 'event' | null;
|
|
15
|
+
direction?: 'inbound' | 'outbound';
|
|
16
|
+
contactId: string | null;
|
|
17
|
+
/** One line: what happened, as a person would read it. */
|
|
18
|
+
text: string;
|
|
19
|
+
/** A message's body. */
|
|
20
|
+
body?: string;
|
|
21
|
+
subject?: string | null;
|
|
22
|
+
status?: string;
|
|
23
|
+
/** The text or email run it belongs to (a message it answered, an action it ran). */
|
|
24
|
+
runId?: string | null;
|
|
25
|
+
/** Where this entry is in the order: pass it as `before` for what comes before it (the next page). */
|
|
26
|
+
cursor: string;
|
|
27
|
+
};
|
|
28
|
+
/**
|
|
29
|
+
* What a timeline read may narrow to. `before`: an entry's `cursor` — the entries after it in the order
|
|
30
|
+
* (older, or as old and further along), so equal times are never skipped or read twice; or an ISO time.
|
|
31
|
+
*/
|
|
32
|
+
export type TimelineWhere = {
|
|
33
|
+
kinds?: TimelineKind[];
|
|
34
|
+
channel?: 'call' | 'sms' | 'email';
|
|
35
|
+
contactId?: string;
|
|
36
|
+
before?: string;
|
|
37
|
+
/** Default 50, at most 200. */
|
|
38
|
+
limit?: number;
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=timeline.d.ts.map
|