@superpanel-sdk/runtime 0.2.3 → 0.5.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 +28 -8
- package/dist/builder/codegen/emit.d.ts +29 -11
- package/dist/builder/codegen/emit.js +328 -61
- package/dist/builder/codegen/generate.d.ts +21 -0
- package/dist/builder/codegen/generate.js +152 -20
- 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 +24 -0
- package/dist/builder/company-imports.js +114 -0
- package/dist/builder/compile/compile-sequence.d.ts +59 -8
- package/dist/builder/compile/compile-sequence.js +279 -77
- package/dist/builder/compile/function-code.d.ts +12 -2
- package/dist/builder/compile/function-code.js +87 -39
- package/dist/builder/compile/registry.d.ts +7 -3
- package/dist/builder/compile/registry.js +24 -23
- package/dist/builder/load.d.ts +70 -10
- package/dist/builder/load.js +250 -61
- package/dist/builder/paths.d.ts +3 -0
- package/dist/builder/paths.js +16 -0
- package/dist/builder/schedules.d.ts +23 -0
- package/dist/builder/schedules.js +102 -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 +105 -0
- package/dist/builder/types.d.ts +91 -19
- package/dist/builder/types.js +2 -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/compile/index.js +3 -0
- package/dist/cli/config.d.ts +10 -0
- package/dist/cli/config.js +52 -0
- package/dist/cli/index.js +28 -8
- package/dist/cli/serve/index.d.ts +73 -3
- package/dist/cli/serve/index.js +365 -116
- package/dist/cli/type-gen/index.js +9 -8
- package/dist/compiler.d.ts +7 -0
- package/dist/compiler.js +7 -0
- package/dist/compute-host/compute-host.d.ts +15 -23
- package/dist/compute-host/compute-host.js +25 -28
- 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 +11 -9
- package/dist/compute-host/source-compute-host.js +46 -32
- package/dist/core/case-orchestrator/contact-hours.d.ts +50 -0
- package/dist/core/case-orchestrator/contact-hours.js +108 -0
- package/dist/core/case-orchestrator/follow-ups.d.ts +31 -0
- package/dist/core/case-orchestrator/follow-ups.js +103 -0
- package/dist/core/case-orchestrator/index.d.ts +6 -0
- package/dist/core/case-orchestrator/index.js +6 -0
- package/dist/core/case-orchestrator/render.d.ts +75 -0
- package/dist/core/case-orchestrator/render.js +239 -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/tools.d.ts +29 -0
- package/dist/core/case-orchestrator/tools.js +31 -0
- package/dist/core/case-orchestrator/types.d.ts +753 -0
- package/dist/core/case-orchestrator/types.js +46 -0
- package/dist/core/case-orchestrator/validate.d.ts +11 -0
- package/dist/core/case-orchestrator/validate.js +63 -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 +159 -0
- package/dist/core/context/email-message.js +59 -0
- package/dist/core/context/email.d.ts +97 -0
- package/dist/core/context/email.js +91 -0
- package/dist/core/context/exchanges.d.ts +142 -0
- package/dist/core/context/exchanges.js +63 -0
- package/dist/core/context/instances.d.ts +278 -0
- package/dist/core/context/instances.js +123 -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 +509 -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 +147 -0
- package/dist/core/context/sms.js +57 -0
- package/dist/core/context/types.d.ts +51 -48
- package/dist/core/context/types.js +1 -7
- package/dist/core/conversation/index.d.ts +31 -0
- package/dist/core/conversation/index.js +14 -0
- package/dist/core/functions/define/bind.d.ts +25 -0
- package/dist/core/functions/define/bind.js +14 -0
- package/dist/core/functions/define/case.d.ts +9 -0
- package/dist/core/functions/define/case.js +13 -0
- package/dist/core/functions/define/events.d.ts +20 -0
- package/dist/core/functions/define/events.js +21 -0
- package/dist/core/functions/define/no-case.d.ts +4 -0
- package/dist/core/functions/define/no-case.js +9 -0
- package/dist/core/functions/events.d.ts +247 -0
- package/dist/core/functions/events.js +32 -0
- package/dist/core/functions/func-ref.d.ts +11 -2
- package/dist/core/functions/schedules.d.ts +51 -0
- package/dist/core/functions/schedules.js +20 -0
- package/dist/core/functions/types.d.ts +62 -51
- package/dist/core/functions/types.js +6 -1
- 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 +834 -0
- package/dist/core/page/define.js +477 -0
- package/dist/core/page/ops.d.ts +58 -0
- package/dist/core/page/ops.js +220 -0
- package/dist/core/page/render.d.ts +253 -0
- package/dist/core/page/render.js +840 -0
- package/dist/core/page/time-format.d.ts +19 -0
- package/dist/core/page/time-format.js +41 -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 +642 -0
- package/dist/core/page/typed.js +2 -0
- package/dist/core/page/workspace.d.ts +33 -0
- package/dist/core/page/workspace.js +231 -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 +137 -0
- package/dist/core/party/schema.js +80 -0
- package/dist/core/party/types.d.ts +40 -0
- package/dist/core/party/types.js +15 -0
- package/dist/core/sequence/define.d.ts +57 -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 +113 -0
- package/dist/core/slot/company-metadata.js +563 -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 -24
- package/dist/core/slot/field.js +0 -69
- 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 +16 -0
- package/dist/core/slot/schema-compat.js +112 -0
- package/dist/core/slot/schema.d.ts +409 -76
- package/dist/core/slot/schema.js +178 -10
- package/dist/core/slot/types.d.ts +27 -94
- package/dist/core/slot/value.d.ts +26 -3
- package/dist/core/slot/value.js +89 -57
- package/dist/dev-overlay/protocol.d.ts +41 -8
- package/dist/fn-context/build-context.d.ts +3 -1
- package/dist/fn-context/build-context.js +20 -10
- package/dist/fn-context/fake/empty-api.js +9 -28
- package/dist/fn-context/fake/index.d.ts +11 -4
- package/dist/fn-context/fake/index.js +27 -2
- package/dist/fn-context/fn-params.d.ts +6 -0
- package/dist/fn-context/fn-params.js +13 -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 +18 -8
- package/dist/fn-context/rpc/case-orchestrator-tools.d.ts +24 -0
- package/dist/fn-context/rpc/case-orchestrator-tools.js +13 -0
- package/dist/fn-context/rpc/index.d.ts +3 -1
- package/dist/fn-context/rpc/index.js +3 -1
- package/dist/fn-context/rpc/integration/index.d.ts +4 -4
- 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/platform.d.ts +2 -2
- package/dist/fn-context/rpc/platform.js +1 -1
- package/dist/fn-context/rpc/records.d.ts +78 -0
- package/dist/fn-context/rpc/records.js +26 -0
- package/dist/fn-context/rpc/trpc-client.d.ts +2 -2
- package/dist/fn-context/rpc/trpc-client.js +88 -7
- package/dist/fn-context/rpc/trpc-init.d.ts +2 -2
- package/dist/fn-context/rpc/trpc-router-factory.d.ts +49 -3
- 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 +36 -3
- package/dist/index.d.ts +37 -46
- package/dist/index.js +33 -33
- package/dist/logger.d.ts +7 -0
- package/dist/logger.js +41 -0
- package/dist/platform.d.ts +76 -0
- package/dist/platform.js +56 -0
- package/dist/voice-service/actions.d.ts +71 -4
- package/dist/voice-service/types.d.ts +42 -5
- package/dist/worker.d.ts +10 -0
- package/dist/worker.js +10 -0
- package/package.json +38 -19
- 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/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,477 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The page module (15 §10.2–§10.6): what a sequence's `context.page.ts` is written with. It runs
|
|
3
|
+
* inside the QuickJS sandbox, bundled into the page script, so it imports nothing at runtime and only
|
|
4
|
+
* uses `globalThis.__host` (the sandbox's async host call, services `page/sandbox.ts`).
|
|
5
|
+
*
|
|
6
|
+
* A page script reads with `db` and returns a free-form document: objects are blocks, arrays are
|
|
7
|
+
* lists, a slot is a cell, `field(slot, …)` a cell with page settings, other plain values read-only
|
|
8
|
+
* content, and actions (`createRow`, `deleteRow`, `defineAction`) buttons. The platform turns the
|
|
9
|
+
* document into a `RenderedPage` (`render.ts`).
|
|
10
|
+
*/
|
|
11
|
+
function runsOf(runs, key, scope) {
|
|
12
|
+
return (scope === 'case' ? runs?.caseActions : runs?.actions)?.[key];
|
|
13
|
+
}
|
|
14
|
+
/** Whether the action `key` ran at least once (whatever its result), in this run or (`'case'`) on the case. */
|
|
15
|
+
export function tried(runs, key, scope = 'run') {
|
|
16
|
+
return (runsOf(runs, key, scope)?.length ?? 0) > 0;
|
|
17
|
+
}
|
|
18
|
+
/** The latest run of the action `key`, in this run or (`'case'`) on the case, if any. */
|
|
19
|
+
export function lastActionRun(runs, key, scope = 'run') {
|
|
20
|
+
return runsOf(runs, key, scope)?.at(-1) ?? null;
|
|
21
|
+
}
|
|
22
|
+
function agree(options) {
|
|
23
|
+
const channels = [...new Set(options?.channels ?? [])];
|
|
24
|
+
if (!channels.length || channels.some((c) => c !== 'call' && c !== 'sms' && c !== 'email')) {
|
|
25
|
+
throw new Error("followUps.agree({ channels }): name at least one of 'call', 'sms', 'email'");
|
|
26
|
+
}
|
|
27
|
+
const action = { $action: 'followUp', key: options.key ?? 'followUp', channels };
|
|
28
|
+
if (options.label !== undefined)
|
|
29
|
+
action.label = options.label;
|
|
30
|
+
if (options.description !== undefined)
|
|
31
|
+
action.description = options.description;
|
|
32
|
+
return action;
|
|
33
|
+
}
|
|
34
|
+
/** The agreed follow-ups with the button that books one. */
|
|
35
|
+
export function pageFollowUps(list) {
|
|
36
|
+
return Object.assign([...list], { agree });
|
|
37
|
+
}
|
|
38
|
+
/** At most this many FAQ entries in a conversation (24 §7.2): the catalog is in every prompt. */
|
|
39
|
+
export const MAX_FAQ_ENTRIES = 100;
|
|
40
|
+
export function choose(rows, label) {
|
|
41
|
+
if (!Array.isArray(rows))
|
|
42
|
+
throw new Error('choose(rows, label): rows must be a list of rows read from db');
|
|
43
|
+
let table = null;
|
|
44
|
+
const options = rows.map((row) => {
|
|
45
|
+
if (!isRow(row))
|
|
46
|
+
throw new Error('choose(rows, label): each row must be a row read from db');
|
|
47
|
+
const ref = Object.values(row).find(isSlot)?.ref;
|
|
48
|
+
if (ref) {
|
|
49
|
+
if (table !== null && table !== ref.table)
|
|
50
|
+
throw new Error(`choose(): rows of ${table} and ${ref.table} together`);
|
|
51
|
+
table = ref.table;
|
|
52
|
+
}
|
|
53
|
+
return { id: row.id, label: String(label(row)) };
|
|
54
|
+
});
|
|
55
|
+
return { $choose: true, table, options };
|
|
56
|
+
}
|
|
57
|
+
export function isChoose(value) {
|
|
58
|
+
return typeof value === 'object' && value !== null && value.$choose === true && Array.isArray(value.options);
|
|
59
|
+
}
|
|
60
|
+
function message(channel, key, options) {
|
|
61
|
+
if (!key || typeof key !== 'string')
|
|
62
|
+
throw new Error(`messages.${channel === 'sms' ? 'text' : 'email'}(): name it with a key`);
|
|
63
|
+
const action = { $action: 'message', channel, key };
|
|
64
|
+
if (options.to !== undefined) {
|
|
65
|
+
const to = typeof options.to === 'string' ? options.to : isRow(options.to) ? options.to.id : null;
|
|
66
|
+
if (!to)
|
|
67
|
+
throw new Error(`messages: ${key}: \`to\` must be a contact row read from db, or its id`);
|
|
68
|
+
action.to = to;
|
|
69
|
+
}
|
|
70
|
+
for (const name of ['body', 'subject', 'say', 'label', 'description']) {
|
|
71
|
+
const value = options[name];
|
|
72
|
+
if (value !== undefined)
|
|
73
|
+
action[name] = value;
|
|
74
|
+
}
|
|
75
|
+
return action;
|
|
76
|
+
}
|
|
77
|
+
export const messages = {
|
|
78
|
+
text: (key, options = {}) => message('sms', key, options),
|
|
79
|
+
email: (key, options = {}) => message('email', key, options),
|
|
80
|
+
};
|
|
81
|
+
export function transferTo(options) {
|
|
82
|
+
const mode = options.mode ?? 'cold';
|
|
83
|
+
if (options.bridge !== undefined && mode !== 'background')
|
|
84
|
+
throw new Error(`transferTo: bridge is for a background transfer (mode '${mode}')`);
|
|
85
|
+
const action = { $action: 'channel', channel: 'transfer', to: options.to, mode };
|
|
86
|
+
if (options.sipAuth !== undefined && !isSipUri(options.to))
|
|
87
|
+
throw new Error(`transferTo: sipAuth is for a SIP URI, not ${options.to}`);
|
|
88
|
+
if (options.sipAuth !== undefined)
|
|
89
|
+
action.sipAuth = { ...options.sipAuth };
|
|
90
|
+
if (options.callerId !== undefined)
|
|
91
|
+
action.callerId = options.callerId;
|
|
92
|
+
if (options.briefing !== undefined)
|
|
93
|
+
action.briefing = options.briefing;
|
|
94
|
+
if (options.bridge !== undefined)
|
|
95
|
+
action.bridge = { ...options.bridge };
|
|
96
|
+
if (options.label !== undefined)
|
|
97
|
+
action.label = options.label;
|
|
98
|
+
if (options.description !== undefined)
|
|
99
|
+
action.description = options.description;
|
|
100
|
+
return action;
|
|
101
|
+
}
|
|
102
|
+
/** Whether a transfer's `to` is a SIP URI rather than a phone number. */
|
|
103
|
+
export function isSipUri(to) {
|
|
104
|
+
return /^sips?:/i.test(to.trim());
|
|
105
|
+
}
|
|
106
|
+
export function keypad(key, options) {
|
|
107
|
+
if (!options.digits.length)
|
|
108
|
+
throw new Error(`keypad('${key}'): digits must name at least one key`);
|
|
109
|
+
const action = { $action: 'channel', channel: 'keypad', key, say: options.say, digits: [...options.digits], timeoutMs: options.timeoutMs ?? 8000 };
|
|
110
|
+
if (options.write !== undefined)
|
|
111
|
+
action.write = isSlot(options.write) ? { ...options.write.ref } : { ...options.write };
|
|
112
|
+
if (options.map !== undefined)
|
|
113
|
+
action.map = { ...options.map };
|
|
114
|
+
if (options.label !== undefined)
|
|
115
|
+
action.label = options.label;
|
|
116
|
+
if (options.description !== undefined)
|
|
117
|
+
action.description = options.description;
|
|
118
|
+
return action;
|
|
119
|
+
}
|
|
120
|
+
/** A company function as a button (20 §6.2): `runFunction('importOrders', { label, say, parameters })`. */
|
|
121
|
+
export function runFunction(name, options = {}) {
|
|
122
|
+
if (!name || typeof name !== 'string')
|
|
123
|
+
throw new Error('runFunction(): name the function (one of functions/)');
|
|
124
|
+
const action = { $action: 'function', name };
|
|
125
|
+
if (options.label !== undefined)
|
|
126
|
+
action.label = options.label;
|
|
127
|
+
if (options.description !== undefined)
|
|
128
|
+
action.description = options.description;
|
|
129
|
+
if (options.say !== undefined)
|
|
130
|
+
action.say = options.say;
|
|
131
|
+
if (options.parameters !== undefined)
|
|
132
|
+
action.parameters = options.parameters;
|
|
133
|
+
if (options.prefill !== undefined) {
|
|
134
|
+
for (const key of Object.keys(options.prefill)) {
|
|
135
|
+
if (!(key in (options.parameters?.properties ?? {})))
|
|
136
|
+
throw new Error(`runFunction('${name}'): ${key} is not one of its parameters`);
|
|
137
|
+
}
|
|
138
|
+
action.prefill = toPrefill(options.prefill);
|
|
139
|
+
}
|
|
140
|
+
return action;
|
|
141
|
+
}
|
|
142
|
+
export function announce(key, text) {
|
|
143
|
+
return { $action: 'channel', channel: 'announce', key, text };
|
|
144
|
+
}
|
|
145
|
+
export function voicemail(text, options = {}) {
|
|
146
|
+
if (typeof text !== 'string' || !text.trim())
|
|
147
|
+
throw new Error('voicemail(text): the message to leave');
|
|
148
|
+
const action = { $action: 'channel', channel: 'voicemail', text };
|
|
149
|
+
if (options.label !== undefined)
|
|
150
|
+
action.label = options.label;
|
|
151
|
+
if (options.description !== undefined)
|
|
152
|
+
action.description = options.description;
|
|
153
|
+
return action;
|
|
154
|
+
}
|
|
155
|
+
export function end(options = {}) {
|
|
156
|
+
const action = { $action: 'channel', channel: 'end' };
|
|
157
|
+
if (options.label !== undefined)
|
|
158
|
+
action.label = options.label;
|
|
159
|
+
if (options.description !== undefined)
|
|
160
|
+
action.description = options.description;
|
|
161
|
+
return action;
|
|
162
|
+
}
|
|
163
|
+
export function perform(action, options = {}) {
|
|
164
|
+
const plain = typeof action.toJSON === 'function' && !('$action' in action) ? action.toJSON() : action;
|
|
165
|
+
if (plain.$action === 'channel' && options.wait === false)
|
|
166
|
+
throw new Error('perform(): a channel action cannot run in the background');
|
|
167
|
+
const out = { $perform: true, action: plain };
|
|
168
|
+
if (options.key !== undefined)
|
|
169
|
+
out.key = options.key;
|
|
170
|
+
if (options.wait !== undefined)
|
|
171
|
+
out.wait = options.wait;
|
|
172
|
+
return out;
|
|
173
|
+
}
|
|
174
|
+
// ---------------------------------------------------------------------------------------------
|
|
175
|
+
function isSlot(value) {
|
|
176
|
+
return (typeof value === 'object' &&
|
|
177
|
+
value !== null &&
|
|
178
|
+
'ref' in value &&
|
|
179
|
+
'state' in value &&
|
|
180
|
+
typeof value.ref === 'object' &&
|
|
181
|
+
value.ref !== null);
|
|
182
|
+
}
|
|
183
|
+
function isRow(value) {
|
|
184
|
+
return typeof value === 'object' && value !== null && typeof value.id === 'string';
|
|
185
|
+
}
|
|
186
|
+
/** Every channel a page may be written for, in order. */
|
|
187
|
+
export const PAGE_CHANNELS = ['call', 'sms', 'email'];
|
|
188
|
+
/**
|
|
189
|
+
* `context.page.ts` (17b §9.1, 24 §4): `definePage({ persona: (ctx) => '…', faq: (ctx) => [...], call:
|
|
190
|
+
* async (ctx) => page, sms: … })`. One page function per channel; what they share is the company code's
|
|
191
|
+
* own business.
|
|
192
|
+
*/
|
|
193
|
+
export function definePage(definition) {
|
|
194
|
+
if (!definition || typeof definition !== 'object' || typeof definition === 'function') {
|
|
195
|
+
throw new Error('definePage({ persona, faq, call, sms, email }): one page function per channel');
|
|
196
|
+
}
|
|
197
|
+
const channels = {};
|
|
198
|
+
for (const channel of PAGE_CHANNELS) {
|
|
199
|
+
const fn = definition[channel];
|
|
200
|
+
if (fn === undefined)
|
|
201
|
+
continue;
|
|
202
|
+
if (typeof fn !== 'function')
|
|
203
|
+
throw new Error(`definePage(): ${channel} must be a page function`);
|
|
204
|
+
channels[channel] = fn;
|
|
205
|
+
}
|
|
206
|
+
if (Object.keys(channels).length === 0)
|
|
207
|
+
throw new Error('definePage(): no page function (call, sms, email)');
|
|
208
|
+
if (typeof definition.persona !== 'function') {
|
|
209
|
+
throw new Error('definePage(): persona must be a function, called once when the conversation starts: persona: (ctx) => \'You are …\' (24 §4)');
|
|
210
|
+
}
|
|
211
|
+
if (definition.faq !== undefined && typeof definition.faq !== 'function') {
|
|
212
|
+
throw new Error('definePage(): faq must be a function, called once when the conversation starts: faq: (ctx) => [{ id, description, content }] (24 §4)');
|
|
213
|
+
}
|
|
214
|
+
return { kind: 'page', persona: definition.persona, faq: definition.faq ?? null, channels };
|
|
215
|
+
}
|
|
216
|
+
/**
|
|
217
|
+
* What `persona` and `faq` returned, checked (24 §4): a persona's text; at most `MAX_FAQ_ENTRIES` FAQ
|
|
218
|
+
* entries, each with an id of its own, a description and an answer. Throws what is wrong.
|
|
219
|
+
*/
|
|
220
|
+
export function checkPageStart(raw) {
|
|
221
|
+
const value = raw;
|
|
222
|
+
if (typeof value?.persona !== 'string' || !value.persona.trim())
|
|
223
|
+
throw new Error('persona(ctx) must return the persona\'s text');
|
|
224
|
+
const faq = value.faq ?? [];
|
|
225
|
+
if (!Array.isArray(faq))
|
|
226
|
+
throw new Error('faq(ctx) must return a list of { id, description, content }');
|
|
227
|
+
if (faq.length > MAX_FAQ_ENTRIES)
|
|
228
|
+
throw new Error(`faq(ctx) returned ${faq.length} entries; at most ${MAX_FAQ_ENTRIES} (24 §7.2)`);
|
|
229
|
+
const seen = new Set();
|
|
230
|
+
const entries = faq.map((entry, i) => {
|
|
231
|
+
const e = entry;
|
|
232
|
+
for (const key of ['id', 'description', 'content']) {
|
|
233
|
+
if (typeof e?.[key] !== 'string' || !e[key].trim())
|
|
234
|
+
throw new Error(`faq(ctx)[${i}]: ${key} must be text`);
|
|
235
|
+
}
|
|
236
|
+
if (seen.has(e.id))
|
|
237
|
+
throw new Error(`faq(ctx): the id ${e.id} is used twice`);
|
|
238
|
+
seen.add(e.id);
|
|
239
|
+
return { id: e.id, description: e.description, content: e.content };
|
|
240
|
+
});
|
|
241
|
+
return { persona: value.persona, faq: entries };
|
|
242
|
+
}
|
|
243
|
+
/**
|
|
244
|
+
* A cell the AI may write (19-page-writable-cells): a slot placed on a page as is is read-only;
|
|
245
|
+
* `field(slot)` opens it, unless `readOnly: true`. The row id and the parent never open; a lookup
|
|
246
|
+
* chooses among its table's rows on the page.
|
|
247
|
+
*/
|
|
248
|
+
/**
|
|
249
|
+
* A ref field (a lookup) on the page (26 §16.2 #7): the rows the AI may set it to, each with its label
|
|
250
|
+
* (`options: [{ id, label }]`, or `choose(rows, label)`); read-only, `options` only labels the current
|
|
251
|
+
* value. Its definition is always on the page. `field()` refuses a ref field.
|
|
252
|
+
*/
|
|
253
|
+
export function refField(slot, options = {}) {
|
|
254
|
+
if (!isSlot(slot))
|
|
255
|
+
throw new Error('refField(): the first argument must be a slot read from db');
|
|
256
|
+
const given = options.options ?? [];
|
|
257
|
+
const list = isChoose(given) ? given.options : given;
|
|
258
|
+
if (!Array.isArray(list))
|
|
259
|
+
throw new Error('refField(): options must be a list of { id, label } or choose(rows, label)');
|
|
260
|
+
const cell = {
|
|
261
|
+
$refField: true,
|
|
262
|
+
slot,
|
|
263
|
+
options: list.map((option) => {
|
|
264
|
+
if (!option || typeof option.id !== 'string')
|
|
265
|
+
throw new Error('refField(): each option is { id, label }, id a row id');
|
|
266
|
+
return { id: option.id, label: String(option.label) };
|
|
267
|
+
}),
|
|
268
|
+
};
|
|
269
|
+
if (options.readOnly !== undefined)
|
|
270
|
+
cell.readOnly = options.readOnly;
|
|
271
|
+
return cell;
|
|
272
|
+
}
|
|
273
|
+
export function field(slot, options = {}) {
|
|
274
|
+
if (!isSlot(slot))
|
|
275
|
+
throw new Error('field(): the first argument must be a slot read from db');
|
|
276
|
+
if (options.hint !== undefined || options.extraction !== undefined) {
|
|
277
|
+
throw new Error("field(): how to fill a field is the page's: write it in $extraction on the page's root, by table.field (26 §15.3)");
|
|
278
|
+
}
|
|
279
|
+
const cell = { $field: true, slot };
|
|
280
|
+
if (options.readOnly !== undefined)
|
|
281
|
+
cell.readOnly = options.readOnly;
|
|
282
|
+
if (options.required !== undefined)
|
|
283
|
+
throw new Error('field(): `required` was removed (24 §2.3): say what is still needed in $guide');
|
|
284
|
+
if (options.expand !== undefined)
|
|
285
|
+
cell.expand = options.expand;
|
|
286
|
+
return cell;
|
|
287
|
+
}
|
|
288
|
+
/** A parameter taken from the schema: `schemaField('providers.name')`, `schemaField('injuries.id')` (a row). */
|
|
289
|
+
export function schemaField(path, options = {}) {
|
|
290
|
+
if (options.description !== undefined)
|
|
291
|
+
throw new Error(`schemaField('${path}'): \`description\` was renamed \`extraction\``);
|
|
292
|
+
const param = { $schemaField: path };
|
|
293
|
+
if (options.extraction !== undefined)
|
|
294
|
+
param.extraction = options.extraction;
|
|
295
|
+
return param;
|
|
296
|
+
}
|
|
297
|
+
function placeable(make) {
|
|
298
|
+
const place = ((prefill) => make(toPrefill((prefill ?? {}))));
|
|
299
|
+
place.toJSON = () => make({});
|
|
300
|
+
return place;
|
|
301
|
+
}
|
|
302
|
+
const CREATE_ROW_OPTIONS = new Set(['fields', 'required', 'label', 'description']);
|
|
303
|
+
/** Add a row (15 §10.6): `fields` are what the AI fills; call it to preset the rest (`({ status: 'active' })`). */
|
|
304
|
+
export function createRow(table, options) {
|
|
305
|
+
const tableName = typeof table === 'string' ? table : table.$table;
|
|
306
|
+
// An option it does not know (a `prefill` here, say) would otherwise be dropped: preset values are
|
|
307
|
+
// given by calling it, `createRow(…)({ parent: row.id })` (26 §16.2 #5d).
|
|
308
|
+
const unknown = Object.keys(options).filter((key) => !CREATE_ROW_OPTIONS.has(key));
|
|
309
|
+
if (unknown.length) {
|
|
310
|
+
throw new Error(`createRow('${tableName}'): unknown option ${unknown.join(', ')} (it takes ${[...CREATE_ROW_OPTIONS].join(', ')}); preset values by calling it: createRow('${tableName}', { … })({ field: value })`);
|
|
311
|
+
}
|
|
312
|
+
return placeable((prefill) => {
|
|
313
|
+
const action = {
|
|
314
|
+
$action: 'createRow',
|
|
315
|
+
table: tableName,
|
|
316
|
+
fields: [...options.fields],
|
|
317
|
+
required: [...(options.required ?? [])],
|
|
318
|
+
prefill,
|
|
319
|
+
};
|
|
320
|
+
if (options.label !== undefined)
|
|
321
|
+
action.label = options.label;
|
|
322
|
+
if (options.description !== undefined)
|
|
323
|
+
action.description = options.description;
|
|
324
|
+
return action;
|
|
325
|
+
});
|
|
326
|
+
}
|
|
327
|
+
/** Delete a row (cascading, 15 §10.6.1): a built-in action with the row prefilled and nothing to fill. */
|
|
328
|
+
export function deleteRow(row, options = {}) {
|
|
329
|
+
const rowId = typeof row === 'string' ? row : isRow(row) ? row.id : null;
|
|
330
|
+
if (!rowId)
|
|
331
|
+
throw new Error('deleteRow(): the first argument must be a row read from db, or its id');
|
|
332
|
+
const action = { $action: 'deleteRow', rowId };
|
|
333
|
+
if (options.reason !== undefined)
|
|
334
|
+
action.reason = options.reason;
|
|
335
|
+
if (options.label !== undefined)
|
|
336
|
+
action.label = options.label;
|
|
337
|
+
if (options.description !== undefined)
|
|
338
|
+
action.description = options.description;
|
|
339
|
+
return action;
|
|
340
|
+
}
|
|
341
|
+
const ACTIONS = new Map();
|
|
342
|
+
/**
|
|
343
|
+
* A custom action, declared like an LLM function call: `parameters` are everything `run` takes. Where
|
|
344
|
+
* the page places it, some parameters may be prefilled; the AI fills the rest.
|
|
345
|
+
*/
|
|
346
|
+
export function defineAction(definition) {
|
|
347
|
+
if (ACTIONS.has(definition.name))
|
|
348
|
+
throw new Error(`defineAction(): two actions are named ${definition.name}`);
|
|
349
|
+
ACTIONS.set(definition.name, { run: definition.run });
|
|
350
|
+
const parameters = definition.parameters ?? { type: 'object', properties: {} };
|
|
351
|
+
const place = placeable((prefill) => {
|
|
352
|
+
for (const key of Object.keys(prefill)) {
|
|
353
|
+
if (!(key in parameters.properties))
|
|
354
|
+
throw new Error(`${definition.name}: ${key} is not one of its parameters`);
|
|
355
|
+
}
|
|
356
|
+
return { $action: 'custom', name: definition.name, description: definition.description, parameters, prefill };
|
|
357
|
+
});
|
|
358
|
+
return Object.assign(place, { actionName: definition.name, run: definition.run });
|
|
359
|
+
}
|
|
360
|
+
/** Prefilled values as given (ids and plain JSON, as `parameters` types them), without the unset ones. */
|
|
361
|
+
function toPrefill(prefill) {
|
|
362
|
+
const out = {};
|
|
363
|
+
for (const [key, value] of Object.entries(prefill)) {
|
|
364
|
+
if (value !== undefined)
|
|
365
|
+
out[key] = value;
|
|
366
|
+
}
|
|
367
|
+
return out;
|
|
368
|
+
}
|
|
369
|
+
function host() {
|
|
370
|
+
const call = globalThis.__host;
|
|
371
|
+
if (!call)
|
|
372
|
+
throw new Error('the page module runs inside the page sandbox only');
|
|
373
|
+
return call;
|
|
374
|
+
}
|
|
375
|
+
function dbProxy(mode) {
|
|
376
|
+
const call = host();
|
|
377
|
+
return new Proxy({}, {
|
|
378
|
+
get(_, table) {
|
|
379
|
+
if (typeof table !== 'string')
|
|
380
|
+
return undefined;
|
|
381
|
+
const read = (method) => (args) => call('records', [table, method, args ?? {}]);
|
|
382
|
+
const reads = {
|
|
383
|
+
$table: table,
|
|
384
|
+
findUnique: read('findUnique'),
|
|
385
|
+
findFirst: read('findFirst'),
|
|
386
|
+
findMany: read('findMany'),
|
|
387
|
+
count: read('count'),
|
|
388
|
+
};
|
|
389
|
+
if (mode === 'read')
|
|
390
|
+
return reads;
|
|
391
|
+
const write = (method) => (args) => call('write', [table, method, args ?? {}]);
|
|
392
|
+
return { ...reads, create: write('create'), update: write('update'), upsert: write('upsert'), delete: write('delete') };
|
|
393
|
+
},
|
|
394
|
+
});
|
|
395
|
+
}
|
|
396
|
+
/** `ctx.time` inside the sandbox: `format` and `day` are host calls (the sandbox has no `Intl`). */
|
|
397
|
+
function timeOf(at) {
|
|
398
|
+
const call = host();
|
|
399
|
+
return {
|
|
400
|
+
zone: at.zone,
|
|
401
|
+
today: at.today,
|
|
402
|
+
format: (when, options = {}) => call('time', [when, { timeZone: options.timeZone ?? at.zone, ...(options.style ? { style: options.style } : {}) }]),
|
|
403
|
+
day: (when, timeZone) => call('day', [when, timeZone ?? at.zone]),
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
/** `history` inside the sandbox: each count is a `history` host call. */
|
|
407
|
+
function historyProxy() {
|
|
408
|
+
const call = host();
|
|
409
|
+
return {
|
|
410
|
+
sessionCount: (where) => call('history', ['sessionCount', where ?? {}]),
|
|
411
|
+
exchangeCount: (where) => call('history', ['exchangeCount', where ?? {}]),
|
|
412
|
+
timeline: (where) => call('history', ['timeline', where ?? {}]),
|
|
413
|
+
};
|
|
414
|
+
}
|
|
415
|
+
/** No runs yet. */
|
|
416
|
+
export function emptyRuns() {
|
|
417
|
+
return { actions: {}, caseActions: {} };
|
|
418
|
+
}
|
|
419
|
+
/** What the compiled page script evaluates to: renders the page, or runs one of its actions. */
|
|
420
|
+
export function pageEntry(mod) {
|
|
421
|
+
const exported = (mod.default ?? mod.page);
|
|
422
|
+
return (arg) => {
|
|
423
|
+
if (arg.mode === 'channels') {
|
|
424
|
+
if (exported?.kind !== 'page')
|
|
425
|
+
throw new Error('context.page.ts must default-export definePage({ persona, faq, call, sms, email })');
|
|
426
|
+
return { channels: Object.keys(exported.channels ?? {}) };
|
|
427
|
+
}
|
|
428
|
+
if (arg.mode === 'render' || arg.mode === 'start') {
|
|
429
|
+
const channels = exported?.kind === 'page' ? (exported.channels ?? {}) : null;
|
|
430
|
+
if (!channels)
|
|
431
|
+
throw new Error('context.page.ts must default-export definePage({ persona, faq, call, sms, email })');
|
|
432
|
+
const channel = arg.channel ?? Object.keys(channels)[0] ?? null;
|
|
433
|
+
const main = channel ? channels[channel] : undefined;
|
|
434
|
+
if (!channel || typeof main !== 'function')
|
|
435
|
+
throw new Error(`context.page.ts has no page function for ${arg.channel ?? 'any channel'}`);
|
|
436
|
+
const caps = channel === 'call'
|
|
437
|
+
? { call: { transferTo, keypad, announce, voicemail, previous: arg.call?.previous ?? null, farEnd: arg.call?.farEnd ?? null } }
|
|
438
|
+
: channel === 'sms'
|
|
439
|
+
? { sms: { inbound: arg.sms?.inbound ?? [] } }
|
|
440
|
+
: { email: { inbound: arg.email?.inbound ?? [], mailbox: arg.email?.mailbox ?? '' } };
|
|
441
|
+
const base = {
|
|
442
|
+
db: dbProxy('read'),
|
|
443
|
+
runs: arg.runs ?? emptyRuns(),
|
|
444
|
+
scheduledAction: arg.scheduledAction ?? null,
|
|
445
|
+
officeHours: arg.officeHours ?? null,
|
|
446
|
+
followUps: pageFollowUps(arg.followUps ?? []),
|
|
447
|
+
subject: arg.subject,
|
|
448
|
+
caller: arg.caller,
|
|
449
|
+
session: arg.session,
|
|
450
|
+
conversation: arg.conversation,
|
|
451
|
+
history: historyProxy(),
|
|
452
|
+
now: arg.now,
|
|
453
|
+
messages,
|
|
454
|
+
time: timeOf(arg.time ?? { zone: 'America/New_York', today: arg.now.slice(0, 10) }),
|
|
455
|
+
};
|
|
456
|
+
const ctx = { ...base, ...caps, channel };
|
|
457
|
+
if (arg.mode === 'start') {
|
|
458
|
+
const page = exported;
|
|
459
|
+
return Promise.all([page.persona(ctx), page.faq ? page.faq(ctx) : []]).then(([persona, faq]) => ({ persona, faq }));
|
|
460
|
+
}
|
|
461
|
+
return main(ctx);
|
|
462
|
+
}
|
|
463
|
+
if (arg.mode !== 'action')
|
|
464
|
+
throw new Error(`unknown page mode ${String(arg.mode)}`);
|
|
465
|
+
const action = ACTIONS.get(arg.name);
|
|
466
|
+
if (!action)
|
|
467
|
+
throw new Error(`no action named ${arg.name}`);
|
|
468
|
+
return action.run(arg.args, {
|
|
469
|
+
db: dbProxy('readWrite'),
|
|
470
|
+
subject: arg.subject,
|
|
471
|
+
caller: arg.caller,
|
|
472
|
+
session: arg.session ?? null,
|
|
473
|
+
now: arg.now,
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
//# sourceMappingURL=define.js.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { SlotState } from './define.js';
|
|
2
|
+
import type { RenderedAction, RenderedCell, RenderedPage } from './render.js';
|
|
3
|
+
import type { RowNames } from './render.js';
|
|
4
|
+
/** `add` / `remove`: options added to or taken from a `multi_select`'s value, the rest kept (27 §3.3). */
|
|
5
|
+
export type SlotOp = {
|
|
6
|
+
slot: string;
|
|
7
|
+
value?: unknown;
|
|
8
|
+
add?: unknown;
|
|
9
|
+
remove?: unknown;
|
|
10
|
+
state?: SlotState;
|
|
11
|
+
reason?: string;
|
|
12
|
+
};
|
|
13
|
+
export type ActionOp = {
|
|
14
|
+
action: string;
|
|
15
|
+
arguments?: Record<string, unknown>;
|
|
16
|
+
};
|
|
17
|
+
export type OpenOp = {
|
|
18
|
+
open: string;
|
|
19
|
+
why?: string;
|
|
20
|
+
};
|
|
21
|
+
export type PageOp = SlotOp | ActionOp | OpenOp;
|
|
22
|
+
export type PageOpsAllowed = {
|
|
23
|
+
/** Slot ids the AI may change (the page's writable cells). */
|
|
24
|
+
slots: readonly string[];
|
|
25
|
+
/** Action ids the AI may press. */
|
|
26
|
+
actions: readonly string[];
|
|
27
|
+
/** Block paths the AI may open; empty: no `open`. */
|
|
28
|
+
open: readonly string[];
|
|
29
|
+
};
|
|
30
|
+
export type ResolvedSlotOp = {
|
|
31
|
+
kind: 'slot';
|
|
32
|
+
cell: RenderedCell;
|
|
33
|
+
value: unknown;
|
|
34
|
+
/** `empty`: cleared (the customer took it back). */
|
|
35
|
+
state: SlotState;
|
|
36
|
+
reason: string | null;
|
|
37
|
+
};
|
|
38
|
+
/** `args`: what the AI filled, rows as ids; the page's prefilled arguments are added when it runs. */
|
|
39
|
+
export type ResolvedActionOp = {
|
|
40
|
+
kind: 'action';
|
|
41
|
+
action: RenderedAction;
|
|
42
|
+
args: Record<string, unknown>;
|
|
43
|
+
};
|
|
44
|
+
export type ResolvedOpenOp = {
|
|
45
|
+
kind: 'open';
|
|
46
|
+
path: string;
|
|
47
|
+
why: string;
|
|
48
|
+
};
|
|
49
|
+
export type ResolvedOp = ResolvedSlotOp | ResolvedActionOp | ResolvedOpenOp;
|
|
50
|
+
export type DroppedOp = {
|
|
51
|
+
op: unknown;
|
|
52
|
+
why: string;
|
|
53
|
+
};
|
|
54
|
+
export declare function resolvePageOps(page: RenderedPage, names: RowNames, answer: unknown, allowed: PageOpsAllowed): {
|
|
55
|
+
ops: ResolvedOp[];
|
|
56
|
+
dropped: DroppedOp[];
|
|
57
|
+
};
|
|
58
|
+
//# sourceMappingURL=ops.d.ts.map
|