@ai-ecoverse/slicc-agent 3.3.0 → 3.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 +37 -1
- package/dist/control.js +44 -1
- package/dist/control.js.map +2 -2
- package/dist/host.js +5 -1
- package/dist/host.js.map +2 -2
- package/dist/kernel/activity.js +39 -0
- package/dist/kernel/activity.js.map +7 -0
- package/dist/kernel/env.js +16 -8
- package/dist/kernel/env.js.map +2 -2
- package/dist/licks/boot.js +66 -0
- package/dist/licks/boot.js.map +7 -0
- package/dist/licks/config.js +126 -0
- package/dist/licks/config.js.map +7 -0
- package/dist/licks/cron.js +120 -0
- package/dist/licks/cron.js.map +7 -0
- package/dist/licks/extension.js +123 -0
- package/dist/licks/extension.js.map +7 -0
- package/dist/licks/fswatch.js +156 -0
- package/dist/licks/fswatch.js.map +7 -0
- package/dist/licks/glob.js +47 -0
- package/dist/licks/glob.js.map +7 -0
- package/dist/licks/index.js +36 -0
- package/dist/licks/index.js.map +7 -0
- package/dist/licks/lick.js +107 -0
- package/dist/licks/lick.js.map +7 -0
- package/dist/licks/licks.js +194 -0
- package/dist/licks/licks.js.map +7 -0
- package/dist/licks/schedules.js +50 -0
- package/dist/licks/schedules.js.map +7 -0
- package/dist/licks/sources.js +139 -0
- package/dist/licks/sources.js.map +7 -0
- package/dist/licks/state.js +56 -0
- package/dist/licks/state.js.map +7 -0
- package/dist/main.js +23 -4
- package/dist/main.js.map +2 -2
- package/dist/services.js.map +2 -2
- package/dist/spectrum/agent-port.js +6 -1
- package/dist/spectrum/agent-port.js.map +2 -2
- package/dist/spectrum/messages.js +29 -5
- package/dist/spectrum/messages.js.map +2 -2
- package/dist/system.js +16 -0
- package/dist/system.js.map +2 -2
- package/dist/types/control.d.ts +6 -1
- package/dist/types/host.d.ts +2 -0
- package/dist/types/kernel/activity.d.ts +10 -0
- package/dist/types/kernel/env.d.ts +2 -0
- package/dist/types/licks/boot.d.ts +9 -0
- package/dist/types/licks/config.d.ts +35 -0
- package/dist/types/licks/cron.d.ts +16 -0
- package/dist/types/licks/extension.d.ts +22 -0
- package/dist/types/licks/fswatch.d.ts +20 -0
- package/dist/types/licks/glob.d.ts +1 -0
- package/dist/types/licks/index.d.ts +24 -0
- package/dist/types/licks/lick.d.ts +23 -0
- package/dist/types/licks/licks.d.ts +58 -0
- package/dist/types/licks/schedules.d.ts +25 -0
- package/dist/types/licks/sources.d.ts +38 -0
- package/dist/types/licks/state.d.ts +14 -0
- package/dist/types/services.d.ts +18 -0
- package/dist/types/spectrum/agent-port.d.ts +1 -1
- package/dist/types/system.d.ts +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
const SPECIAL = /[.+^$()|\\]/;
|
|
2
|
+
function braces(pattern, at) {
|
|
3
|
+
const end = pattern.indexOf("}", at);
|
|
4
|
+
if (end < 0) return void 0;
|
|
5
|
+
const options = pattern.slice(at + 1, end).split(",");
|
|
6
|
+
if (options.length < 2) return void 0;
|
|
7
|
+
return [`(?:${options.map((option) => source(option)).join("|")})`, end];
|
|
8
|
+
}
|
|
9
|
+
function klass(pattern, at) {
|
|
10
|
+
const end = pattern.indexOf("]", at + 2);
|
|
11
|
+
if (end < 0) return void 0;
|
|
12
|
+
const inner = pattern.slice(at + 1, end);
|
|
13
|
+
const negated = inner.startsWith("!");
|
|
14
|
+
const body = (negated ? inner.slice(1) : inner).replace(/[\\\]^]/g, "\\$&");
|
|
15
|
+
return [`[${negated ? "^/" : ""}${body}]`, end];
|
|
16
|
+
}
|
|
17
|
+
function source(pattern) {
|
|
18
|
+
let out = "";
|
|
19
|
+
for (let at = 0; at < pattern.length; at++) {
|
|
20
|
+
const char = pattern[at];
|
|
21
|
+
if (char === "*" && pattern[at + 1] === "*") {
|
|
22
|
+
const slash = pattern[at + 2] === "/";
|
|
23
|
+
out += slash ? "(?:.*/)?" : ".*";
|
|
24
|
+
at += slash ? 2 : 1;
|
|
25
|
+
} else if (char === "*") out += "[^/]*";
|
|
26
|
+
else if (char === "?") out += "[^/]";
|
|
27
|
+
else if (char === "{" && braces(pattern, at)) {
|
|
28
|
+
const [group, end] = braces(pattern, at);
|
|
29
|
+
out += group;
|
|
30
|
+
at = end;
|
|
31
|
+
} else if (char === "[" && klass(pattern, at)) {
|
|
32
|
+
const [group, end] = klass(pattern, at);
|
|
33
|
+
out += group;
|
|
34
|
+
at = end;
|
|
35
|
+
} else
|
|
36
|
+
out += SPECIAL.test(char) || char === "{" || char === "}" || char === "[" || char === "]" ? `\\${char}` : char;
|
|
37
|
+
}
|
|
38
|
+
return out;
|
|
39
|
+
}
|
|
40
|
+
function glob(pattern) {
|
|
41
|
+
const expression = new RegExp(`^${source(pattern)}$`);
|
|
42
|
+
return (path) => expression.test(path);
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
glob
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=glob.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/licks/glob.ts"],
|
|
4
|
+
"sourcesContent": ["const SPECIAL = /[.+^$()|\\\\]/;\n\nfunction braces(pattern: string, at: number): [string, number] | undefined {\n const end = pattern.indexOf('}', at);\n if (end < 0) return undefined;\n const options = pattern.slice(at + 1, end).split(',');\n if (options.length < 2) return undefined;\n return [`(?:${options.map((option) => source(option)).join('|')})`, end];\n}\n\nfunction klass(pattern: string, at: number): [string, number] | undefined {\n const end = pattern.indexOf(']', at + 2);\n if (end < 0) return undefined;\n const inner = pattern.slice(at + 1, end);\n const negated = inner.startsWith('!');\n const body = (negated ? inner.slice(1) : inner).replace(/[\\\\\\]^]/g, '\\\\$&');\n return [`[${negated ? '^/' : ''}${body}]`, end];\n}\n\nfunction source(pattern: string): string {\n let out = '';\n for (let at = 0; at < pattern.length; at++) {\n const char = pattern[at] as string;\n if (char === '*' && pattern[at + 1] === '*') {\n const slash = pattern[at + 2] === '/';\n out += slash ? '(?:.*/)?' : '.*';\n at += slash ? 2 : 1;\n } else if (char === '*') out += '[^/]*';\n else if (char === '?') out += '[^/]';\n else if (char === '{' && braces(pattern, at)) {\n const [group, end] = braces(pattern, at) as [string, number];\n out += group;\n at = end;\n } else if (char === '[' && klass(pattern, at)) {\n const [group, end] = klass(pattern, at) as [string, number];\n out += group;\n at = end;\n } else\n out +=\n SPECIAL.test(char) || char === '{' || char === '}' || char === '[' || char === ']'\n ? `\\\\${char}`\n : char;\n }\n return out;\n}\n\nexport function glob(pattern: string): (path: string) => boolean {\n const expression = new RegExp(`^${source(pattern)}$`);\n return (path) => expression.test(path);\n}\n"],
|
|
5
|
+
"mappings": "AAAA,MAAM,UAAU;AAEhB,SAAS,OAAO,SAAiB,IAA0C;AACzE,QAAM,MAAM,QAAQ,QAAQ,KAAK,EAAE;AACnC,MAAI,MAAM,EAAG,QAAO;AACpB,QAAM,UAAU,QAAQ,MAAM,KAAK,GAAG,GAAG,EAAE,MAAM,GAAG;AACpD,MAAI,QAAQ,SAAS,EAAG,QAAO;AAC/B,SAAO,CAAC,MAAM,QAAQ,IAAI,CAAC,WAAW,OAAO,MAAM,CAAC,EAAE,KAAK,GAAG,CAAC,KAAK,GAAG;AACzE;AAEA,SAAS,MAAM,SAAiB,IAA0C;AACxE,QAAM,MAAM,QAAQ,QAAQ,KAAK,KAAK,CAAC;AACvC,MAAI,MAAM,EAAG,QAAO;AACpB,QAAM,QAAQ,QAAQ,MAAM,KAAK,GAAG,GAAG;AACvC,QAAM,UAAU,MAAM,WAAW,GAAG;AACpC,QAAM,QAAQ,UAAU,MAAM,MAAM,CAAC,IAAI,OAAO,QAAQ,YAAY,MAAM;AAC1E,SAAO,CAAC,IAAI,UAAU,OAAO,EAAE,GAAG,IAAI,KAAK,GAAG;AAChD;AAEA,SAAS,OAAO,SAAyB;AACvC,MAAI,MAAM;AACV,WAAS,KAAK,GAAG,KAAK,QAAQ,QAAQ,MAAM;AAC1C,UAAM,OAAO,QAAQ,EAAE;AACvB,QAAI,SAAS,OAAO,QAAQ,KAAK,CAAC,MAAM,KAAK;AAC3C,YAAM,QAAQ,QAAQ,KAAK,CAAC,MAAM;AAClC,aAAO,QAAQ,aAAa;AAC5B,YAAM,QAAQ,IAAI;AAAA,IACpB,WAAW,SAAS,IAAK,QAAO;AAAA,aACvB,SAAS,IAAK,QAAO;AAAA,aACrB,SAAS,OAAO,OAAO,SAAS,EAAE,GAAG;AAC5C,YAAM,CAAC,OAAO,GAAG,IAAI,OAAO,SAAS,EAAE;AACvC,aAAO;AACP,WAAK;AAAA,IACP,WAAW,SAAS,OAAO,MAAM,SAAS,EAAE,GAAG;AAC7C,YAAM,CAAC,OAAO,GAAG,IAAI,MAAM,SAAS,EAAE;AACtC,aAAO;AACP,WAAK;AAAA,IACP;AACE,aACE,QAAQ,KAAK,IAAI,KAAK,SAAS,OAAO,SAAS,OAAO,SAAS,OAAO,SAAS,MAC3E,KAAK,IAAI,KACT;AAAA,EACV;AACA,SAAO;AACT;AAEO,SAAS,KAAK,SAA4C;AAC/D,QAAM,aAAa,IAAI,OAAO,IAAI,OAAO,OAAO,CAAC,GAAG;AACpD,SAAO,CAAC,SAAS,WAAW,KAAK,IAAI;AACvC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { cronTask, licksExtension } from "./extension.js";
|
|
2
|
+
import { createLicks } from "./licks.js";
|
|
3
|
+
import { createLickSources } from "./sources.js";
|
|
4
|
+
function setupLicks(registry) {
|
|
5
|
+
let bind = () => void 0;
|
|
6
|
+
const host = new Promise((resolve) => {
|
|
7
|
+
bind = resolve;
|
|
8
|
+
});
|
|
9
|
+
const licks = createLicks(host);
|
|
10
|
+
const cron = cronTask(licks);
|
|
11
|
+
const lookup = async (id, context) => {
|
|
12
|
+
const found = await (await host).harness.conversation(id, context);
|
|
13
|
+
if (!found) throw new Error(`conversation ${id} is gone`);
|
|
14
|
+
return found;
|
|
15
|
+
};
|
|
16
|
+
registry.install(licksExtension(licks, lookup, cron));
|
|
17
|
+
return {
|
|
18
|
+
licks,
|
|
19
|
+
attach(agent, options) {
|
|
20
|
+
const { harness, root } = agent;
|
|
21
|
+
bind({ harness, resolve: async (_target) => agent.cone() });
|
|
22
|
+
return createLickSources({
|
|
23
|
+
...options,
|
|
24
|
+
harness,
|
|
25
|
+
root,
|
|
26
|
+
cone: () => agent.cone(),
|
|
27
|
+
licks,
|
|
28
|
+
cron
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
export {
|
|
34
|
+
setupLicks
|
|
35
|
+
};
|
|
36
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/licks/index.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport type { Conversation, ConversationId, Harness, Registry } from '@earendil-works/pi-durable';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport type { Activity } from '../kernel/activity.ts';\nimport { cronTask, licksExtension } from './extension.ts';\nimport type { LickTarget } from './lick.ts';\nimport { createLicks, type Licks, type LicksHost } from './licks.ts';\nimport { createLickSources, type LickSources } from './sources.ts';\n\nexport interface LicksAgent {\n harness: Harness;\n root: Conversation;\n cone(): Conversation;\n}\n\nexport interface LicksSetup {\n licks: Licks;\n attach(\n agent: LicksAgent,\n options: {\n env: ExecutionEnv;\n home: string;\n now?: () => number;\n flushEvery?: number;\n activity?: Activity;\n }\n ): LickSources;\n}\n\nexport function setupLicks(registry: Registry): LicksSetup {\n let bind: (host: LicksHost) => void = () => undefined;\n const host = new Promise<LicksHost>((resolve) => {\n bind = resolve;\n });\n const licks = createLicks(host);\n const cron = cronTask(licks);\n const lookup = async (id: ConversationId, context: Context) => {\n const found = await (await host).harness.conversation(id, context);\n if (!found) throw new Error(`conversation ${id} is gone`);\n return found;\n };\n registry.install(licksExtension(licks, lookup, cron));\n return {\n licks,\n attach(agent, options) {\n const { harness, root } = agent;\n bind({ harness, resolve: async (_target: LickTarget) => agent.cone() });\n return createLickSources({\n ...options,\n harness,\n root,\n cone: () => agent.cone(),\n licks,\n cron,\n });\n },\n };\n}\n\nexport type { Lick, LickChannel } from './lick.ts';\nexport type { LickEvent, Licks } from './licks.ts';\nexport type { LickSources, WebhookDelivery } from './sources.ts';\n"],
|
|
5
|
+
"mappings": "AAIA,SAAS,UAAU,sBAAsB;AAEzC,SAAS,mBAA+C;AACxD,SAAS,yBAA2C;AAsB7C,SAAS,WAAW,UAAgC;AACzD,MAAI,OAAkC,MAAM;AAC5C,QAAM,OAAO,IAAI,QAAmB,CAAC,YAAY;AAC/C,WAAO;AAAA,EACT,CAAC;AACD,QAAM,QAAQ,YAAY,IAAI;AAC9B,QAAM,OAAO,SAAS,KAAK;AAC3B,QAAM,SAAS,OAAO,IAAoB,YAAqB;AAC7D,UAAM,QAAQ,OAAO,MAAM,MAAM,QAAQ,aAAa,IAAI,OAAO;AACjE,QAAI,CAAC,MAAO,OAAM,IAAI,MAAM,gBAAgB,EAAE,UAAU;AACxD,WAAO;AAAA,EACT;AACA,WAAS,QAAQ,eAAe,OAAO,QAAQ,IAAI,CAAC;AACpD,SAAO;AAAA,IACL;AAAA,IACA,OAAO,OAAO,SAAS;AACrB,YAAM,EAAE,SAAS,KAAK,IAAI;AAC1B,WAAK,EAAE,SAAS,SAAS,OAAO,YAAwB,MAAM,KAAK,EAAE,CAAC;AACtE,aAAO,kBAAkB;AAAA,QACvB,GAAG;AAAA,QACH;AAAA,QACA;AAAA,QACA,MAAM,MAAM,MAAM,KAAK;AAAA,QACvB;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
const LICK_CHANNELS = [
|
|
2
|
+
"webhook",
|
|
3
|
+
"cron",
|
|
4
|
+
"sprinkle",
|
|
5
|
+
"fswatch",
|
|
6
|
+
"session-reload",
|
|
7
|
+
"navigate",
|
|
8
|
+
"discovery",
|
|
9
|
+
"upgrade",
|
|
10
|
+
"workflow",
|
|
11
|
+
"bash",
|
|
12
|
+
"jshd",
|
|
13
|
+
"preview",
|
|
14
|
+
"cherry",
|
|
15
|
+
"scoop-notify",
|
|
16
|
+
"scoop-idle",
|
|
17
|
+
"scoop-wait",
|
|
18
|
+
"sudo-request"
|
|
19
|
+
];
|
|
20
|
+
const ENTITIES = {
|
|
21
|
+
"&": "&",
|
|
22
|
+
"<": "<",
|
|
23
|
+
">": ">",
|
|
24
|
+
'"': """,
|
|
25
|
+
"\n": " ",
|
|
26
|
+
"\r": " "
|
|
27
|
+
};
|
|
28
|
+
const DECODE = Object.fromEntries(
|
|
29
|
+
Object.entries(ENTITIES).map(([raw, entity]) => [entity, raw])
|
|
30
|
+
);
|
|
31
|
+
function escapeText(value) {
|
|
32
|
+
return value.replace(/[&<>]/g, (char) => ENTITIES[char]).replace(/\r/g, " ");
|
|
33
|
+
}
|
|
34
|
+
function escapeAttribute(value) {
|
|
35
|
+
return value.replace(/[&<>"\n\r]/g, (char) => ENTITIES[char]);
|
|
36
|
+
}
|
|
37
|
+
function unescape(value) {
|
|
38
|
+
return value.replace(/&(?:amp|lt|gt|quot|#10|#13);/g, (entity) => DECODE[entity]);
|
|
39
|
+
}
|
|
40
|
+
function lickId() {
|
|
41
|
+
return `lk-${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`;
|
|
42
|
+
}
|
|
43
|
+
function oneLine(value) {
|
|
44
|
+
return value.replace(/\s+/g, " ").trim();
|
|
45
|
+
}
|
|
46
|
+
function formatLick(lick) {
|
|
47
|
+
const attributes = [
|
|
48
|
+
["id", lick.id],
|
|
49
|
+
["channel", lick.channel],
|
|
50
|
+
["source", lick.source],
|
|
51
|
+
["title", oneLine(lick.title)],
|
|
52
|
+
["count", String(lick.count)],
|
|
53
|
+
["at", new Date(lick.at).toISOString()]
|
|
54
|
+
];
|
|
55
|
+
if (lick.actions?.length) attributes.push(["actions", lick.actions.join(" ")]);
|
|
56
|
+
if (lick.severity) attributes.push(["severity", lick.severity]);
|
|
57
|
+
const head = attributes.map(([name, value]) => `${name}="${escapeAttribute(value)}"`).join(" ");
|
|
58
|
+
const lines = [escapeText(oneLine(lick.text))];
|
|
59
|
+
if (lick.body) lines.push(escapeText(lick.body));
|
|
60
|
+
return `<lick ${head}>
|
|
61
|
+
${lines.join("\n")}
|
|
62
|
+
</lick>`;
|
|
63
|
+
}
|
|
64
|
+
const LICK = /^<lick ((?:[a-z]+="[^"<>\n]*" ?)+)>\n([^\n]*)(?:\n([\s\S]*?))?\n<\/lick>$/;
|
|
65
|
+
const ATTRIBUTE = /([a-z]+)="([^"]*)"/g;
|
|
66
|
+
function isChannel(value) {
|
|
67
|
+
return LICK_CHANNELS.includes(value ?? "");
|
|
68
|
+
}
|
|
69
|
+
function severityOf(value) {
|
|
70
|
+
return value === "warn" || value === "error" ? { severity: value } : {};
|
|
71
|
+
}
|
|
72
|
+
function parseLick(text) {
|
|
73
|
+
const match = LICK.exec(text);
|
|
74
|
+
if (!match) return void 0;
|
|
75
|
+
const attributes = /* @__PURE__ */ new Map();
|
|
76
|
+
for (const [, name, value] of match[1].matchAll(ATTRIBUTE))
|
|
77
|
+
attributes.set(name, unescape(value));
|
|
78
|
+
const channel = attributes.get("channel");
|
|
79
|
+
const id = attributes.get("id");
|
|
80
|
+
if (!id || !isChannel(channel)) return void 0;
|
|
81
|
+
const at = Date.parse(attributes.get("at") ?? "");
|
|
82
|
+
const count = Number(attributes.get("count"));
|
|
83
|
+
const actions = (attributes.get("actions") ?? "").split(" ").filter((action) => action === "confirm" || action === "dismiss");
|
|
84
|
+
const body = match[3] === void 0 ? void 0 : unescape(match[3]);
|
|
85
|
+
return {
|
|
86
|
+
id,
|
|
87
|
+
channel,
|
|
88
|
+
source: attributes.get("source") ?? "",
|
|
89
|
+
title: attributes.get("title") ?? "",
|
|
90
|
+
text: unescape(match[2]),
|
|
91
|
+
...body ? { body } : {},
|
|
92
|
+
count: Number.isSafeInteger(count) && count > 0 ? count : 1,
|
|
93
|
+
at: Number.isNaN(at) ? 0 : at,
|
|
94
|
+
...actions.length ? { actions } : {},
|
|
95
|
+
...severityOf(attributes.get("severity"))
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
export {
|
|
99
|
+
LICK_CHANNELS,
|
|
100
|
+
escapeAttribute,
|
|
101
|
+
escapeText,
|
|
102
|
+
formatLick,
|
|
103
|
+
lickId,
|
|
104
|
+
parseLick,
|
|
105
|
+
unescape
|
|
106
|
+
};
|
|
107
|
+
//# sourceMappingURL=lick.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/licks/lick.ts"],
|
|
4
|
+
"sourcesContent": ["export const LICK_CHANNELS = [\n 'webhook',\n 'cron',\n 'sprinkle',\n 'fswatch',\n 'session-reload',\n 'navigate',\n 'discovery',\n 'upgrade',\n 'workflow',\n 'bash',\n 'jshd',\n 'preview',\n 'cherry',\n 'scoop-notify',\n 'scoop-idle',\n 'scoop-wait',\n 'sudo-request',\n] as const;\n\nexport type LickChannel = (typeof LICK_CHANNELS)[number];\n\nexport type LickAction = 'confirm' | 'dismiss';\n\nexport type LickSeverity = 'warn' | 'error';\n\nexport type LickTarget = 'cone' | `scoop:${string}`;\n\nexport type Lick = {\n id: string;\n channel: LickChannel;\n source: string;\n title: string;\n text: string;\n body?: string;\n count: number;\n at: number;\n actions?: LickAction[];\n severity?: LickSeverity;\n};\n\nconst ENTITIES: Record<string, string> = {\n '&': '&',\n '<': '<',\n '>': '>',\n '\"': '"',\n '\\n': ' ',\n '\\r': ' ',\n};\n\nconst DECODE: Record<string, string> = Object.fromEntries(\n Object.entries(ENTITIES).map(([raw, entity]) => [entity, raw])\n);\n\nexport function escapeText(value: string): string {\n return value.replace(/[&<>]/g, (char) => ENTITIES[char] as string).replace(/\\r/g, ' ');\n}\n\nexport function escapeAttribute(value: string): string {\n return value.replace(/[&<>\"\\n\\r]/g, (char) => ENTITIES[char] as string);\n}\n\nexport function unescape(value: string): string {\n return value.replace(/&(?:amp|lt|gt|quot|#10|#13);/g, (entity) => DECODE[entity] as string);\n}\n\nexport function lickId(): string {\n return `lk-${Date.now().toString(36)}-${crypto.randomUUID().slice(0, 8)}`;\n}\n\nfunction oneLine(value: string): string {\n return value.replace(/\\s+/g, ' ').trim();\n}\n\nexport function formatLick(lick: Lick): string {\n const attributes: [string, string][] = [\n ['id', lick.id],\n ['channel', lick.channel],\n ['source', lick.source],\n ['title', oneLine(lick.title)],\n ['count', String(lick.count)],\n ['at', new Date(lick.at).toISOString()],\n ];\n if (lick.actions?.length) attributes.push(['actions', lick.actions.join(' ')]);\n if (lick.severity) attributes.push(['severity', lick.severity]);\n const head = attributes.map(([name, value]) => `${name}=\"${escapeAttribute(value)}\"`).join(' ');\n const lines = [escapeText(oneLine(lick.text))];\n if (lick.body) lines.push(escapeText(lick.body));\n return `<lick ${head}>\\n${lines.join('\\n')}\\n</lick>`;\n}\n\nconst LICK = /^<lick ((?:[a-z]+=\"[^\"<>\\n]*\" ?)+)>\\n([^\\n]*)(?:\\n([\\s\\S]*?))?\\n<\\/lick>$/;\nconst ATTRIBUTE = /([a-z]+)=\"([^\"]*)\"/g;\n\nfunction isChannel(value: string | undefined): value is LickChannel {\n return (LICK_CHANNELS as readonly string[]).includes(value ?? '');\n}\n\nfunction severityOf(value: string | undefined): { severity?: LickSeverity } {\n return value === 'warn' || value === 'error' ? { severity: value } : {};\n}\n\nexport function parseLick(text: string): Lick | undefined {\n const match = LICK.exec(text);\n if (!match) return undefined;\n const attributes = new Map<string, string>();\n for (const [, name, value] of (match[1] as string).matchAll(ATTRIBUTE))\n attributes.set(name as string, unescape(value as string));\n const channel = attributes.get('channel');\n const id = attributes.get('id');\n if (!id || !isChannel(channel)) return undefined;\n const at = Date.parse(attributes.get('at') ?? '');\n const count = Number(attributes.get('count'));\n const actions = (attributes.get('actions') ?? '')\n .split(' ')\n .filter((action): action is LickAction => action === 'confirm' || action === 'dismiss');\n const body = match[3] === undefined ? undefined : unescape(match[3]);\n return {\n id,\n channel,\n source: attributes.get('source') ?? '',\n title: attributes.get('title') ?? '',\n text: unescape(match[2] as string),\n ...(body ? { body } : {}),\n count: Number.isSafeInteger(count) && count > 0 ? count : 1,\n at: Number.isNaN(at) ? 0 : at,\n ...(actions.length ? { actions } : {}),\n ...severityOf(attributes.get('severity')),\n };\n}\n"],
|
|
5
|
+
"mappings": "AAAO,MAAM,gBAAgB;AAAA,EAC3B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAuBA,MAAM,WAAmC;AAAA,EACvC,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,KAAK;AAAA,EACL,MAAM;AAAA,EACN,MAAM;AACR;AAEA,MAAM,SAAiC,OAAO;AAAA,EAC5C,OAAO,QAAQ,QAAQ,EAAE,IAAI,CAAC,CAAC,KAAK,MAAM,MAAM,CAAC,QAAQ,GAAG,CAAC;AAC/D;AAEO,SAAS,WAAW,OAAuB;AAChD,SAAO,MAAM,QAAQ,UAAU,CAAC,SAAS,SAAS,IAAI,CAAW,EAAE,QAAQ,OAAO,OAAO;AAC3F;AAEO,SAAS,gBAAgB,OAAuB;AACrD,SAAO,MAAM,QAAQ,eAAe,CAAC,SAAS,SAAS,IAAI,CAAW;AACxE;AAEO,SAAS,SAAS,OAAuB;AAC9C,SAAO,MAAM,QAAQ,iCAAiC,CAAC,WAAW,OAAO,MAAM,CAAW;AAC5F;AAEO,SAAS,SAAiB;AAC/B,SAAO,MAAM,KAAK,IAAI,EAAE,SAAS,EAAE,CAAC,IAAI,OAAO,WAAW,EAAE,MAAM,GAAG,CAAC,CAAC;AACzE;AAEA,SAAS,QAAQ,OAAuB;AACtC,SAAO,MAAM,QAAQ,QAAQ,GAAG,EAAE,KAAK;AACzC;AAEO,SAAS,WAAW,MAAoB;AAC7C,QAAM,aAAiC;AAAA,IACrC,CAAC,MAAM,KAAK,EAAE;AAAA,IACd,CAAC,WAAW,KAAK,OAAO;AAAA,IACxB,CAAC,UAAU,KAAK,MAAM;AAAA,IACtB,CAAC,SAAS,QAAQ,KAAK,KAAK,CAAC;AAAA,IAC7B,CAAC,SAAS,OAAO,KAAK,KAAK,CAAC;AAAA,IAC5B,CAAC,MAAM,IAAI,KAAK,KAAK,EAAE,EAAE,YAAY,CAAC;AAAA,EACxC;AACA,MAAI,KAAK,SAAS,OAAQ,YAAW,KAAK,CAAC,WAAW,KAAK,QAAQ,KAAK,GAAG,CAAC,CAAC;AAC7E,MAAI,KAAK,SAAU,YAAW,KAAK,CAAC,YAAY,KAAK,QAAQ,CAAC;AAC9D,QAAM,OAAO,WAAW,IAAI,CAAC,CAAC,MAAM,KAAK,MAAM,GAAG,IAAI,KAAK,gBAAgB,KAAK,CAAC,GAAG,EAAE,KAAK,GAAG;AAC9F,QAAM,QAAQ,CAAC,WAAW,QAAQ,KAAK,IAAI,CAAC,CAAC;AAC7C,MAAI,KAAK,KAAM,OAAM,KAAK,WAAW,KAAK,IAAI,CAAC;AAC/C,SAAO,SAAS,IAAI;AAAA,EAAM,MAAM,KAAK,IAAI,CAAC;AAAA;AAC5C;AAEA,MAAM,OAAO;AACb,MAAM,YAAY;AAElB,SAAS,UAAU,OAAiD;AAClE,SAAQ,cAAoC,SAAS,SAAS,EAAE;AAClE;AAEA,SAAS,WAAW,OAAwD;AAC1E,SAAO,UAAU,UAAU,UAAU,UAAU,EAAE,UAAU,MAAM,IAAI,CAAC;AACxE;AAEO,SAAS,UAAU,MAAgC;AACxD,QAAM,QAAQ,KAAK,KAAK,IAAI;AAC5B,MAAI,CAAC,MAAO,QAAO;AACnB,QAAM,aAAa,oBAAI,IAAoB;AAC3C,aAAW,CAAC,EAAE,MAAM,KAAK,KAAM,MAAM,CAAC,EAAa,SAAS,SAAS;AACnE,eAAW,IAAI,MAAgB,SAAS,KAAe,CAAC;AAC1D,QAAM,UAAU,WAAW,IAAI,SAAS;AACxC,QAAM,KAAK,WAAW,IAAI,IAAI;AAC9B,MAAI,CAAC,MAAM,CAAC,UAAU,OAAO,EAAG,QAAO;AACvC,QAAM,KAAK,KAAK,MAAM,WAAW,IAAI,IAAI,KAAK,EAAE;AAChD,QAAM,QAAQ,OAAO,WAAW,IAAI,OAAO,CAAC;AAC5C,QAAM,WAAW,WAAW,IAAI,SAAS,KAAK,IAC3C,MAAM,GAAG,EACT,OAAO,CAAC,WAAiC,WAAW,aAAa,WAAW,SAAS;AACxF,QAAM,OAAO,MAAM,CAAC,MAAM,SAAY,SAAY,SAAS,MAAM,CAAC,CAAC;AACnE,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,QAAQ,WAAW,IAAI,QAAQ,KAAK;AAAA,IACpC,OAAO,WAAW,IAAI,OAAO,KAAK;AAAA,IAClC,MAAM,SAAS,MAAM,CAAC,CAAW;AAAA,IACjC,GAAI,OAAO,EAAE,KAAK,IAAI,CAAC;AAAA,IACvB,OAAO,OAAO,cAAc,KAAK,KAAK,QAAQ,IAAI,QAAQ;AAAA,IAC1D,IAAI,OAAO,MAAM,EAAE,IAAI,IAAI;AAAA,IAC3B,GAAI,QAAQ,SAAS,EAAE,QAAQ,IAAI,CAAC;AAAA,IACpC,GAAG,WAAW,WAAW,IAAI,UAAU,CAAC;AAAA,EAC1C;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineDoc
|
|
3
|
+
} from "@earendil-works/pi-durable";
|
|
4
|
+
import {
|
|
5
|
+
formatLick,
|
|
6
|
+
lickId
|
|
7
|
+
} from "./lick.js";
|
|
8
|
+
import { entryLick, LICK_STATE_KIND, lickDecisions } from "./state.js";
|
|
9
|
+
const LicksOutbox = defineDoc({
|
|
10
|
+
kind: "slicc.licks",
|
|
11
|
+
version: 1,
|
|
12
|
+
scope: "session",
|
|
13
|
+
initial: () => ({ queues: {}, events: {} })
|
|
14
|
+
});
|
|
15
|
+
const MAX_ITEMS = 50;
|
|
16
|
+
const MAX_EVENTS = 64;
|
|
17
|
+
function render(pending) {
|
|
18
|
+
const shown = pending.items.slice(0, MAX_ITEMS);
|
|
19
|
+
const more = pending.items.length - shown.length;
|
|
20
|
+
const lines = [
|
|
21
|
+
...pending.body ? [pending.body] : [],
|
|
22
|
+
...shown,
|
|
23
|
+
...more > 0 ? [`\u2026 and ${more} more`] : []
|
|
24
|
+
];
|
|
25
|
+
const { body: _, ...lick } = pending.lick;
|
|
26
|
+
return lines.length ? { ...lick, body: lines.join("\n") } : lick;
|
|
27
|
+
}
|
|
28
|
+
const RANK = { warn: 1, error: 2 };
|
|
29
|
+
function strongest(a, b) {
|
|
30
|
+
if (!a) return b;
|
|
31
|
+
if (!b) return a;
|
|
32
|
+
return RANK[b] > RANK[a] ? b : a;
|
|
33
|
+
}
|
|
34
|
+
function merge(pending, event, now) {
|
|
35
|
+
const items = [...pending.items];
|
|
36
|
+
for (const item of event.items ?? []) if (!items.includes(item)) items.push(item);
|
|
37
|
+
const { severity: _, ...lick } = pending.lick;
|
|
38
|
+
const severity = strongest(pending.lick.severity, event.severity);
|
|
39
|
+
return {
|
|
40
|
+
lick: {
|
|
41
|
+
...lick,
|
|
42
|
+
title: event.title,
|
|
43
|
+
text: event.text,
|
|
44
|
+
count: pending.lick.count + (event.count ?? 1),
|
|
45
|
+
at: event.at ?? now,
|
|
46
|
+
...severity ? { severity } : {}
|
|
47
|
+
},
|
|
48
|
+
items,
|
|
49
|
+
body: event.body ?? pending.body
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function fresh(event, now, listed) {
|
|
53
|
+
return {
|
|
54
|
+
lick: {
|
|
55
|
+
id: lickId(),
|
|
56
|
+
channel: event.channel,
|
|
57
|
+
source: event.source,
|
|
58
|
+
title: event.title,
|
|
59
|
+
text: event.text,
|
|
60
|
+
count: event.count ?? 1,
|
|
61
|
+
at: now,
|
|
62
|
+
...listed ? { actions: listed } : {},
|
|
63
|
+
...event.severity ? { severity: event.severity } : {}
|
|
64
|
+
},
|
|
65
|
+
items: [...new Set(event.items ?? [])],
|
|
66
|
+
body: event.body ?? null
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
function keyOf(event) {
|
|
70
|
+
const base = `${event.target}|${event.channel}|${event.source}`;
|
|
71
|
+
return event.coalesce === false ? `${base}|${event.eventId ?? lickId()}` : base;
|
|
72
|
+
}
|
|
73
|
+
function createLicks(host) {
|
|
74
|
+
const handlers = /* @__PURE__ */ new Map();
|
|
75
|
+
let lock = Promise.resolve();
|
|
76
|
+
const serial = (operation) => {
|
|
77
|
+
const run = lock.then(operation, operation);
|
|
78
|
+
lock = run.catch(() => void 0);
|
|
79
|
+
return run;
|
|
80
|
+
};
|
|
81
|
+
const actions = (channel) => {
|
|
82
|
+
const handler = handlers.get(channel);
|
|
83
|
+
const list = [];
|
|
84
|
+
if (handler?.confirm) list.push("confirm");
|
|
85
|
+
if (handler?.dismiss) list.push("dismiss");
|
|
86
|
+
return list.length ? list : void 0;
|
|
87
|
+
};
|
|
88
|
+
async function previous(harness, id, context) {
|
|
89
|
+
if (id === null) return "gone";
|
|
90
|
+
const submission = await harness.submission(id, context);
|
|
91
|
+
const record = await submission?.status(context);
|
|
92
|
+
if (record?.status === "queued") return "queued";
|
|
93
|
+
return record?.status === "unanswered" && record.reason === "aborted" ? "withdrawn" : "gone";
|
|
94
|
+
}
|
|
95
|
+
async function settled(key, context) {
|
|
96
|
+
const { harness } = await host;
|
|
97
|
+
const queue = (await harness.snapshot(LicksOutbox, context))?.queues[key];
|
|
98
|
+
if (!queue) return void 0;
|
|
99
|
+
const before = await previous(harness, queue.submission, context);
|
|
100
|
+
if (before === "queued") return void 0;
|
|
101
|
+
if (queue.pending && before === "gone") return queue;
|
|
102
|
+
await harness.commit(async (tx) => {
|
|
103
|
+
const doc = await tx.doc(LicksOutbox);
|
|
104
|
+
if (doc.queues[key]?.submission === queue.submission) delete doc.queues[key];
|
|
105
|
+
}, context);
|
|
106
|
+
return void 0;
|
|
107
|
+
}
|
|
108
|
+
async function flushKey(key, context) {
|
|
109
|
+
const { harness, resolve } = await host;
|
|
110
|
+
const queue = await settled(key, context);
|
|
111
|
+
if (!queue?.pending) return;
|
|
112
|
+
const lick = render(queue.pending);
|
|
113
|
+
const target = key.split("|")[0];
|
|
114
|
+
const conversation = await resolve(target, context);
|
|
115
|
+
const submission = await conversation.submit(
|
|
116
|
+
{
|
|
117
|
+
type: "input",
|
|
118
|
+
content: formatLick(lick),
|
|
119
|
+
whenBusy: "steer",
|
|
120
|
+
requestId: `lick:${lick.id}`
|
|
121
|
+
},
|
|
122
|
+
context
|
|
123
|
+
);
|
|
124
|
+
await harness.commit(async (tx) => {
|
|
125
|
+
const doc = await tx.doc(LicksOutbox);
|
|
126
|
+
doc.queues[key] = { submission: submission.id, pending: null };
|
|
127
|
+
}, context);
|
|
128
|
+
}
|
|
129
|
+
async function flushAll(context) {
|
|
130
|
+
const { harness } = await host;
|
|
131
|
+
const keys = Object.keys((await harness.snapshot(LicksOutbox, context))?.queues ?? {});
|
|
132
|
+
for (const key of keys) await flushKey(key, context);
|
|
133
|
+
}
|
|
134
|
+
async function find(conversation, id, context) {
|
|
135
|
+
const { entries } = await conversation.context(context);
|
|
136
|
+
const lick = entries.map(entryLick).find((candidate) => candidate?.id === id);
|
|
137
|
+
if (!lick) return void 0;
|
|
138
|
+
return { lick, decision: lickDecisions(entries).get(id) };
|
|
139
|
+
}
|
|
140
|
+
return {
|
|
141
|
+
deliver(event, context) {
|
|
142
|
+
return serial(async () => {
|
|
143
|
+
const { harness } = await host;
|
|
144
|
+
const key = keyOf(event);
|
|
145
|
+
await settled(key, context);
|
|
146
|
+
const stream = `${event.channel}|${event.source}`;
|
|
147
|
+
const now = event.at ?? Date.now();
|
|
148
|
+
const accepted = await harness.commit(async (tx) => {
|
|
149
|
+
const doc = await tx.doc(LicksOutbox);
|
|
150
|
+
const seen = doc.events[stream] ?? [];
|
|
151
|
+
if (event.eventId !== void 0) {
|
|
152
|
+
if (seen.includes(event.eventId)) return false;
|
|
153
|
+
doc.events[stream] = [...seen, event.eventId].slice(-MAX_EVENTS);
|
|
154
|
+
}
|
|
155
|
+
const queue = doc.queues[key] ?? { submission: null, pending: null };
|
|
156
|
+
queue.pending = queue.pending ? merge(queue.pending, event, now) : fresh(event, now, actions(event.channel));
|
|
157
|
+
doc.queues[key] = queue;
|
|
158
|
+
return true;
|
|
159
|
+
}, context);
|
|
160
|
+
if (accepted) await flushKey(key, context);
|
|
161
|
+
});
|
|
162
|
+
},
|
|
163
|
+
flush: (context) => serial(() => flushAll(context)),
|
|
164
|
+
find,
|
|
165
|
+
async decide(conversation, id, action, reason, context) {
|
|
166
|
+
const found = await find(conversation, id, context);
|
|
167
|
+
if (!found) throw new Error(`There is no lick ${id} in this conversation.`);
|
|
168
|
+
if (found.decision) throw new Error(`Lick ${id} is already ${found.decision}.`);
|
|
169
|
+
if (!found.lick.actions?.includes(action))
|
|
170
|
+
throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);
|
|
171
|
+
const run = handlers.get(found.lick.channel)?.[action];
|
|
172
|
+
if (!run) throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);
|
|
173
|
+
return await run(found.lick, reason, context) ?? `Lick ${id} ${action}ed.`;
|
|
174
|
+
},
|
|
175
|
+
handle(channel, handler) {
|
|
176
|
+
handlers.set(channel, handler);
|
|
177
|
+
},
|
|
178
|
+
async pending(context) {
|
|
179
|
+
const { harness } = await host;
|
|
180
|
+
const queues = (await harness.snapshot(LicksOutbox, context))?.queues ?? {};
|
|
181
|
+
return Object.values(queues).filter((queue) => queue.submission !== null || queue.pending).length;
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
}
|
|
185
|
+
export {
|
|
186
|
+
LICK_STATE_KIND,
|
|
187
|
+
LicksOutbox,
|
|
188
|
+
MAX_EVENTS,
|
|
189
|
+
MAX_ITEMS,
|
|
190
|
+
createLicks,
|
|
191
|
+
render,
|
|
192
|
+
strongest
|
|
193
|
+
};
|
|
194
|
+
//# sourceMappingURL=licks.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/licks/licks.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n defineDoc,\n type Harness,\n type SubmissionId,\n} from '@earendil-works/pi-durable';\nimport {\n formatLick,\n type Lick,\n type LickAction,\n type LickChannel,\n type LickSeverity,\n type LickTarget,\n lickId,\n} from './lick.ts';\nimport { entryLick, LICK_STATE_KIND, type LickDecision, lickDecisions } from './state.ts';\n\nexport interface LickEvent {\n channel: LickChannel;\n source: string;\n title: string;\n text: string;\n body?: string;\n items?: string[];\n count?: number;\n target: LickTarget;\n coalesce?: boolean;\n severity?: LickSeverity;\n eventId?: string;\n at?: number;\n}\n\ntype Pending = {\n lick: Lick;\n items: string[];\n body: string | null;\n};\n\ntype Queue = {\n submission: number | null;\n pending: Pending | null;\n};\n\ntype OutboxState = {\n queues: Record<string, Queue>;\n events: Record<string, string[]>;\n};\n\nexport const LicksOutbox = defineDoc<OutboxState>({\n kind: 'slicc.licks',\n version: 1,\n scope: 'session',\n initial: () => ({ queues: {}, events: {} }),\n});\n\nexport interface LickHandler {\n confirm?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n dismiss?(lick: Lick, reason: string | undefined, context: Context): Promise<string | undefined>;\n}\n\nexport interface LickFound {\n lick: Lick;\n decision: LickDecision | undefined;\n}\n\nexport interface Licks {\n deliver(event: LickEvent, context: Context): Promise<void>;\n flush(context: Context): Promise<void>;\n find(conversation: Conversation, id: string, context: Context): Promise<LickFound | undefined>;\n decide(\n conversation: Conversation,\n id: string,\n action: LickAction,\n reason: string | undefined,\n context: Context\n ): Promise<string>;\n handle(channel: LickChannel, handler: LickHandler): void;\n pending(context: Context): Promise<number>;\n}\n\nexport interface LicksHost {\n harness: Harness;\n resolve(target: LickTarget, context: Context): Promise<Conversation>;\n}\n\nexport const MAX_ITEMS = 50;\nexport const MAX_EVENTS = 64;\n\nexport function render(pending: Pending): Lick {\n const shown = pending.items.slice(0, MAX_ITEMS);\n const more = pending.items.length - shown.length;\n const lines = [\n ...(pending.body ? [pending.body] : []),\n ...shown,\n ...(more > 0 ? [`\u2026 and ${more} more`] : []),\n ];\n const { body: _, ...lick } = pending.lick;\n return lines.length ? { ...lick, body: lines.join('\\n') } : lick;\n}\n\nconst RANK: Record<LickSeverity, number> = { warn: 1, error: 2 };\n\nexport function strongest(\n a: LickSeverity | undefined,\n b: LickSeverity | undefined\n): LickSeverity | undefined {\n if (!a) return b;\n if (!b) return a;\n return RANK[b] > RANK[a] ? b : a;\n}\n\nfunction merge(pending: Pending, event: LickEvent, now: number): Pending {\n const items = [...pending.items];\n for (const item of event.items ?? []) if (!items.includes(item)) items.push(item);\n const { severity: _, ...lick } = pending.lick;\n const severity = strongest(pending.lick.severity, event.severity);\n return {\n lick: {\n ...lick,\n title: event.title,\n text: event.text,\n count: pending.lick.count + (event.count ?? 1),\n at: event.at ?? now,\n ...(severity ? { severity } : {}),\n },\n items,\n body: event.body ?? pending.body,\n };\n}\n\nfunction fresh(event: LickEvent, now: number, listed: LickAction[] | undefined): Pending {\n return {\n lick: {\n id: lickId(),\n channel: event.channel,\n source: event.source,\n title: event.title,\n text: event.text,\n count: event.count ?? 1,\n at: now,\n ...(listed ? { actions: listed } : {}),\n ...(event.severity ? { severity: event.severity } : {}),\n },\n items: [...new Set(event.items ?? [])],\n body: event.body ?? null,\n };\n}\n\nfunction keyOf(event: LickEvent): string {\n const base = `${event.target}|${event.channel}|${event.source}`;\n return event.coalesce === false ? `${base}|${event.eventId ?? lickId()}` : base;\n}\n\nexport function createLicks(host: Promise<LicksHost>): Licks {\n const handlers = new Map<LickChannel, LickHandler>();\n let lock: Promise<unknown> = Promise.resolve();\n const serial = <T>(operation: () => Promise<T>): Promise<T> => {\n const run = lock.then(operation, operation);\n lock = run.catch(() => undefined);\n return run;\n };\n\n const actions = (channel: LickChannel): LickAction[] | undefined => {\n const handler = handlers.get(channel);\n const list: LickAction[] = [];\n if (handler?.confirm) list.push('confirm');\n if (handler?.dismiss) list.push('dismiss');\n return list.length ? list : undefined;\n };\n\n async function previous(\n harness: Harness,\n id: number | null,\n context: Context\n ): Promise<'queued' | 'withdrawn' | 'gone'> {\n if (id === null) return 'gone';\n const submission = await harness.submission(id as SubmissionId, context);\n const record = await submission?.status(context);\n if (record?.status === 'queued') return 'queued';\n return record?.status === 'unanswered' && record.reason === 'aborted' ? 'withdrawn' : 'gone';\n }\n\n async function settled(key: string, context: Context): Promise<Queue | undefined> {\n const { harness } = await host;\n const queue = (await harness.snapshot(LicksOutbox, context))?.queues[key];\n if (!queue) return undefined;\n const before = await previous(harness, queue.submission, context);\n if (before === 'queued') return undefined;\n if (queue.pending && before === 'gone') return queue;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n if (doc.queues[key]?.submission === queue.submission) delete doc.queues[key];\n }, context);\n return undefined;\n }\n\n async function flushKey(key: string, context: Context): Promise<void> {\n const { harness, resolve } = await host;\n const queue = await settled(key, context);\n if (!queue?.pending) return;\n const lick = render(queue.pending);\n const target = key.split('|')[0] as LickTarget;\n const conversation = await resolve(target, context);\n const submission = await conversation.submit(\n {\n type: 'input',\n content: formatLick(lick),\n whenBusy: 'steer',\n requestId: `lick:${lick.id}`,\n },\n context\n );\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n doc.queues[key] = { submission: submission.id, pending: null };\n }, context);\n }\n\n async function flushAll(context: Context): Promise<void> {\n const { harness } = await host;\n const keys = Object.keys((await harness.snapshot(LicksOutbox, context))?.queues ?? {});\n for (const key of keys) await flushKey(key, context);\n }\n\n async function find(\n conversation: Conversation,\n id: string,\n context: Context\n ): Promise<LickFound | undefined> {\n const { entries } = await conversation.context(context);\n const lick = entries.map(entryLick).find((candidate) => candidate?.id === id);\n if (!lick) return undefined;\n return { lick, decision: lickDecisions(entries).get(id) };\n }\n\n return {\n deliver(event, context) {\n return serial(async () => {\n const { harness } = await host;\n const key = keyOf(event);\n await settled(key, context);\n const stream = `${event.channel}|${event.source}`;\n const now = event.at ?? Date.now();\n const accepted = await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksOutbox);\n const seen = doc.events[stream] ?? [];\n if (event.eventId !== undefined) {\n if (seen.includes(event.eventId)) return false;\n doc.events[stream] = [...seen, event.eventId].slice(-MAX_EVENTS);\n }\n const queue = doc.queues[key] ?? { submission: null, pending: null };\n queue.pending = queue.pending\n ? merge(queue.pending, event, now)\n : fresh(event, now, actions(event.channel));\n doc.queues[key] = queue;\n return true;\n }, context);\n if (accepted) await flushKey(key, context);\n });\n },\n flush: (context) => serial(() => flushAll(context)),\n find,\n async decide(conversation, id, action, reason, context) {\n const found = await find(conversation, id, context);\n if (!found) throw new Error(`There is no lick ${id} in this conversation.`);\n if (found.decision) throw new Error(`Lick ${id} is already ${found.decision}.`);\n if (!found.lick.actions?.includes(action))\n throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n const run = handlers.get(found.lick.channel)?.[action];\n if (!run) throw new Error(`Lick ${id} (${found.lick.channel}) has no ${action} action.`);\n return (await run(found.lick, reason, context)) ?? `Lick ${id} ${action}ed.`;\n },\n handle(channel, handler) {\n handlers.set(channel, handler);\n },\n async pending(context) {\n const { harness } = await host;\n const queues = (await harness.snapshot(LicksOutbox, context))?.queues ?? {};\n return Object.values(queues).filter((queue) => queue.submission !== null || queue.pending)\n .length;\n },\n };\n}\n\nexport { LICK_STATE_KIND };\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
defineDoc
|
|
3
|
+
} from "@earendil-works/pi-durable";
|
|
4
|
+
const LicksHostDoc = defineDoc({
|
|
5
|
+
kind: "slicc.licks.host",
|
|
6
|
+
version: 1,
|
|
7
|
+
scope: "session",
|
|
8
|
+
initial: () => ({ cron: {}, agent: null, boot: null, mark: 0, upgrades: 0 })
|
|
9
|
+
});
|
|
10
|
+
function same(entry, record) {
|
|
11
|
+
return entry !== void 0 && entry.schedule === record.schedule && entry.target === record.target && entry.message === record.message;
|
|
12
|
+
}
|
|
13
|
+
async function live(harness, task, context) {
|
|
14
|
+
const record = await harness.getTask(task, context);
|
|
15
|
+
return record !== void 0 && record.state.status !== "terminal";
|
|
16
|
+
}
|
|
17
|
+
async function reconcileCron(options, entries, context) {
|
|
18
|
+
const { harness } = options;
|
|
19
|
+
const wanted = new Map(entries.map((entry) => [entry.name, entry]));
|
|
20
|
+
const stored = (await harness.snapshot(LicksHostDoc, context))?.cron ?? {};
|
|
21
|
+
const stale = [];
|
|
22
|
+
const create = entries.filter((entry) => !stored[entry.name]);
|
|
23
|
+
for (const [name, record] of Object.entries(stored)) {
|
|
24
|
+
const entry = wanted.get(name);
|
|
25
|
+
const running = await live(harness, record.task, context);
|
|
26
|
+
if (same(entry, record) && running) continue;
|
|
27
|
+
stale.push(name);
|
|
28
|
+
if (running) await harness.abortTask(record.task, context);
|
|
29
|
+
if (entry) create.push(entry);
|
|
30
|
+
}
|
|
31
|
+
if (!stale.length && !create.length) return;
|
|
32
|
+
await harness.commit(async (tx) => {
|
|
33
|
+
const doc = await tx.doc(LicksHostDoc);
|
|
34
|
+
for (const name of stale) delete doc.cron[name];
|
|
35
|
+
for (const entry of create) {
|
|
36
|
+
const input = { ...entry, from: options.now() };
|
|
37
|
+
const task = await tx.createTask(options.cron, input, {
|
|
38
|
+
ownership: { kind: "conversation" },
|
|
39
|
+
conversationId: options.root.id,
|
|
40
|
+
background: true
|
|
41
|
+
});
|
|
42
|
+
doc.cron[entry.name] = { ...entry, task };
|
|
43
|
+
}
|
|
44
|
+
}, context);
|
|
45
|
+
}
|
|
46
|
+
export {
|
|
47
|
+
LicksHostDoc,
|
|
48
|
+
reconcileCron
|
|
49
|
+
};
|
|
50
|
+
//# sourceMappingURL=schedules.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../src/licks/schedules.ts"],
|
|
4
|
+
"sourcesContent": ["import type { Context } from '@earendil-works/chord';\nimport {\n type Conversation,\n defineDoc,\n type Harness,\n type TaskId,\n} from '@earendil-works/pi-durable';\nimport type { CronEntry } from './config.ts';\nimport type { CronInput, cronTask } from './extension.ts';\n\ntype CronRecord = {\n schedule: string;\n target: string;\n message: string;\n task: number;\n};\n\ntype HostState = {\n cron: Record<string, CronRecord>;\n agent: string | null;\n boot: string | null;\n mark: number;\n upgrades: number;\n};\n\nexport const LicksHostDoc = defineDoc<HostState>({\n kind: 'slicc.licks.host',\n version: 1,\n scope: 'session',\n initial: () => ({ cron: {}, agent: null, boot: null, mark: 0, upgrades: 0 }),\n});\n\nfunction same(entry: CronEntry | undefined, record: CronRecord): boolean {\n return (\n entry !== undefined &&\n entry.schedule === record.schedule &&\n entry.target === record.target &&\n entry.message === record.message\n );\n}\n\nasync function live(harness: Harness, task: number, context: Context): Promise<boolean> {\n const record = await harness.getTask(task as TaskId, context);\n return record !== undefined && record.state.status !== 'terminal';\n}\n\nexport async function reconcileCron(\n options: {\n harness: Harness;\n root: Conversation;\n cron: ReturnType<typeof cronTask>;\n now: () => number;\n },\n entries: readonly CronEntry[],\n context: Context\n): Promise<void> {\n const { harness } = options;\n const wanted = new Map(entries.map((entry) => [entry.name, entry]));\n const stored: Readonly<Record<string, CronRecord>> =\n (await harness.snapshot(LicksHostDoc, context))?.cron ?? {};\n const stale: string[] = [];\n const create: CronEntry[] = entries.filter((entry) => !stored[entry.name]);\n for (const [name, record] of Object.entries(stored)) {\n const entry = wanted.get(name);\n const running = await live(harness, record.task, context);\n if (same(entry, record) && running) continue;\n stale.push(name);\n if (running) await harness.abortTask(record.task as TaskId, context);\n if (entry) create.push(entry);\n }\n if (!stale.length && !create.length) return;\n await harness.commit(async (tx) => {\n const doc = await tx.doc(LicksHostDoc);\n for (const name of stale) delete doc.cron[name];\n for (const entry of create) {\n const input: CronInput = { ...entry, from: options.now() };\n const task = await tx.createTask(options.cron, input, {\n ownership: { kind: 'conversation' },\n conversationId: options.root.id,\n background: true,\n });\n doc.cron[entry.name] = { ...entry, task };\n }\n }, context);\n}\n"],
|
|
5
|
+
"mappings": "AACA;AAAA,EAEE;AAAA,OAGK;AAmBA,MAAM,eAAe,UAAqB;AAAA,EAC/C,MAAM;AAAA,EACN,SAAS;AAAA,EACT,OAAO;AAAA,EACP,SAAS,OAAO,EAAE,MAAM,CAAC,GAAG,OAAO,MAAM,MAAM,MAAM,MAAM,GAAG,UAAU,EAAE;AAC5E,CAAC;AAED,SAAS,KAAK,OAA8B,QAA6B;AACvE,SACE,UAAU,UACV,MAAM,aAAa,OAAO,YAC1B,MAAM,WAAW,OAAO,UACxB,MAAM,YAAY,OAAO;AAE7B;AAEA,eAAe,KAAK,SAAkB,MAAc,SAAoC;AACtF,QAAM,SAAS,MAAM,QAAQ,QAAQ,MAAgB,OAAO;AAC5D,SAAO,WAAW,UAAa,OAAO,MAAM,WAAW;AACzD;AAEA,eAAsB,cACpB,SAMA,SACA,SACe;AACf,QAAM,EAAE,QAAQ,IAAI;AACpB,QAAM,SAAS,IAAI,IAAI,QAAQ,IAAI,CAAC,UAAU,CAAC,MAAM,MAAM,KAAK,CAAC,CAAC;AAClE,QAAM,UACH,MAAM,QAAQ,SAAS,cAAc,OAAO,IAAI,QAAQ,CAAC;AAC5D,QAAM,QAAkB,CAAC;AACzB,QAAM,SAAsB,QAAQ,OAAO,CAAC,UAAU,CAAC,OAAO,MAAM,IAAI,CAAC;AACzE,aAAW,CAAC,MAAM,MAAM,KAAK,OAAO,QAAQ,MAAM,GAAG;AACnD,UAAM,QAAQ,OAAO,IAAI,IAAI;AAC7B,UAAM,UAAU,MAAM,KAAK,SAAS,OAAO,MAAM,OAAO;AACxD,QAAI,KAAK,OAAO,MAAM,KAAK,QAAS;AACpC,UAAM,KAAK,IAAI;AACf,QAAI,QAAS,OAAM,QAAQ,UAAU,OAAO,MAAgB,OAAO;AACnE,QAAI,MAAO,QAAO,KAAK,KAAK;AAAA,EAC9B;AACA,MAAI,CAAC,MAAM,UAAU,CAAC,OAAO,OAAQ;AACrC,QAAM,QAAQ,OAAO,OAAO,OAAO;AACjC,UAAM,MAAM,MAAM,GAAG,IAAI,YAAY;AACrC,eAAW,QAAQ,MAAO,QAAO,IAAI,KAAK,IAAI;AAC9C,eAAW,SAAS,QAAQ;AAC1B,YAAM,QAAmB,EAAE,GAAG,OAAO,MAAM,QAAQ,IAAI,EAAE;AACzD,YAAM,OAAO,MAAM,GAAG,WAAW,QAAQ,MAAM,OAAO;AAAA,QACpD,WAAW,EAAE,MAAM,eAAe;AAAA,QAClC,gBAAgB,QAAQ,KAAK;AAAA,QAC7B,YAAY;AAAA,MACd,CAAC;AACD,UAAI,KAAK,MAAM,IAAI,IAAI,EAAE,GAAG,OAAO,KAAK;AAAA,IAC1C;AAAA,EACF,GAAG,OAAO;AACZ;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|