@glows777/vela-channels 0.0.0-stage → 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/CHANGELOG.md +263 -0
- package/LICENSE +21 -0
- package/README.md +4 -2
- package/dist/admission.d.ts +15 -0
- package/dist/admission.d.ts.map +1 -0
- package/dist/admission.js +31 -0
- package/dist/admission.js.map +1 -0
- package/dist/atomic-json.d.ts +3 -0
- package/dist/atomic-json.d.ts.map +1 -0
- package/dist/atomic-json.js +23 -0
- package/dist/atomic-json.js.map +1 -0
- package/dist/commands.d.ts +47 -0
- package/dist/commands.d.ts.map +1 -0
- package/dist/commands.js +130 -0
- package/dist/commands.js.map +1 -0
- package/dist/directory.d.ts +144 -0
- package/dist/directory.d.ts.map +1 -0
- package/dist/directory.js +309 -0
- package/dist/directory.js.map +1 -0
- package/dist/event-receiver.d.ts +19 -0
- package/dist/event-receiver.d.ts.map +1 -0
- package/dist/event-receiver.js +192 -0
- package/dist/event-receiver.js.map +1 -0
- package/dist/events.d.ts +40 -0
- package/dist/events.d.ts.map +1 -0
- package/dist/events.js +247 -0
- package/dist/events.js.map +1 -0
- package/dist/feedback.d.ts +61 -0
- package/dist/feedback.d.ts.map +1 -0
- package/dist/feedback.js +470 -0
- package/dist/feedback.js.map +1 -0
- package/dist/gateway.d.ts +76 -0
- package/dist/gateway.d.ts.map +1 -0
- package/dist/gateway.js +536 -0
- package/dist/gateway.js.map +1 -0
- package/dist/generic-events.d.ts +9 -0
- package/dist/generic-events.d.ts.map +1 -0
- package/dist/generic-events.js +33 -0
- package/dist/generic-events.js.map +1 -0
- package/dist/github-events.d.ts +11 -0
- package/dist/github-events.d.ts.map +1 -0
- package/dist/github-events.js +118 -0
- package/dist/github-events.js.map +1 -0
- package/dist/handoff.d.ts +28 -0
- package/dist/handoff.d.ts.map +1 -0
- package/dist/handoff.js +189 -0
- package/dist/handoff.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/log.d.ts +2 -0
- package/dist/log.d.ts.map +1 -0
- package/dist/log.js +2 -0
- package/dist/log.js.map +1 -0
- package/dist/recovery.d.ts +19 -0
- package/dist/recovery.d.ts.map +1 -0
- package/dist/recovery.js +190 -0
- package/dist/recovery.js.map +1 -0
- package/dist/routine-time.d.ts +8 -0
- package/dist/routine-time.d.ts.map +1 -0
- package/dist/routine-time.js +114 -0
- package/dist/routine-time.js.map +1 -0
- package/dist/routines.d.ts +126 -0
- package/dist/routines.d.ts.map +1 -0
- package/dist/routines.js +370 -0
- package/dist/routines.js.map +1 -0
- package/dist/scope.d.ts +11 -0
- package/dist/scope.d.ts.map +1 -0
- package/dist/scope.js +26 -0
- package/dist/scope.js.map +1 -0
- package/dist/session-id.d.ts +6 -0
- package/dist/session-id.d.ts.map +1 -0
- package/dist/session-id.js +21 -0
- package/dist/session-id.js.map +1 -0
- package/dist/sessions.d.ts +13 -0
- package/dist/sessions.d.ts.map +1 -0
- package/dist/sessions.js +40 -0
- package/dist/sessions.js.map +1 -0
- package/dist/testing.d.ts +4 -0
- package/dist/testing.d.ts.map +1 -0
- package/dist/testing.js +8 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +177 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/usage.d.ts +31 -0
- package/dist/usage.d.ts.map +1 -0
- package/dist/usage.js +151 -0
- package/dist/usage.js.map +1 -0
- package/dist/vela.d.ts +38 -0
- package/dist/vela.d.ts.map +1 -0
- package/dist/vela.js +112 -0
- package/dist/vela.js.map +1 -0
- package/package.json +41 -4
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { RoutineSchedule } from './types.ts';
|
|
2
|
+
export declare function routineTimezone(value?: unknown): string;
|
|
3
|
+
/** A tool's date/time is local to the deployment, never the host's Date parser. */
|
|
4
|
+
export declare function localRoutineInstant(value: string, zone: string): string;
|
|
5
|
+
export declare function routineLocalTime(at: number, zone: string): string;
|
|
6
|
+
export declare function nextOccurrence(schedule: RoutineSchedule, after: number, zone: string): number | undefined;
|
|
7
|
+
export declare function latestOccurrence(schedule: RoutineSchedule, after: number, through: number, zone: string): number | undefined;
|
|
8
|
+
//# sourceMappingURL=routine-time.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routine-time.d.ts","sourceRoot":"","sources":["../src/routine-time.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAA;AAKjD,wBAAgB,eAAe,CAAC,KAAK,CAAC,EAAE,OAAO,GAAG,MAAM,CAgBvD;AA2DD,mFAAmF;AACnF,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAUvE;AAED,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,MAAM,CAKjE;AAgBD,wBAAgB,cAAc,CAC5B,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,SAAS,CAYpB;AAED,wBAAgB,gBAAgB,CAC9B,QAAQ,EAAE,eAAe,EACzB,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,MAAM,EACf,IAAI,EAAE,MAAM,GACX,MAAM,GAAG,SAAS,CAYpB"}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
const weekdays = ['sun', 'mon', 'tue', 'wed', 'thu', 'fri', 'sat'];
|
|
2
|
+
const formatters = new Map();
|
|
3
|
+
export function routineTimezone(value) {
|
|
4
|
+
if (value !== undefined &&
|
|
5
|
+
(typeof value !== 'string' || !value.trim() || /^[+-]/.test(value)))
|
|
6
|
+
throw new Error('Routine timezone must be an IANA timezone name');
|
|
7
|
+
const zone = value === undefined
|
|
8
|
+
? new Intl.DateTimeFormat().resolvedOptions().timeZone
|
|
9
|
+
: value;
|
|
10
|
+
try {
|
|
11
|
+
formatter(zone);
|
|
12
|
+
}
|
|
13
|
+
catch (cause) {
|
|
14
|
+
throw new Error(`Invalid routine timezone ${zone}`, { cause });
|
|
15
|
+
}
|
|
16
|
+
return zone;
|
|
17
|
+
}
|
|
18
|
+
function formatter(zone) {
|
|
19
|
+
let value = formatters.get(zone);
|
|
20
|
+
if (!value) {
|
|
21
|
+
value = new Intl.DateTimeFormat('en-GB', {
|
|
22
|
+
timeZone: zone,
|
|
23
|
+
year: 'numeric',
|
|
24
|
+
month: '2-digit',
|
|
25
|
+
day: '2-digit',
|
|
26
|
+
hour: '2-digit',
|
|
27
|
+
minute: '2-digit',
|
|
28
|
+
second: '2-digit',
|
|
29
|
+
hourCycle: 'h23',
|
|
30
|
+
});
|
|
31
|
+
formatters.set(zone, value);
|
|
32
|
+
}
|
|
33
|
+
return value;
|
|
34
|
+
}
|
|
35
|
+
function localWall(at, zone) {
|
|
36
|
+
const parts = Object.fromEntries(formatter(zone)
|
|
37
|
+
.formatToParts(at)
|
|
38
|
+
.map((p) => [p.type, Number(p.value)]));
|
|
39
|
+
return Date.UTC(parts.year, parts.month - 1, parts.day, parts.hour, parts.minute, parts.second);
|
|
40
|
+
}
|
|
41
|
+
function localDay(at, zone) {
|
|
42
|
+
const day = new Date(localWall(at, zone));
|
|
43
|
+
return Date.UTC(day.getUTCFullYear(), day.getUTCMonth(), day.getUTCDate());
|
|
44
|
+
}
|
|
45
|
+
/** Resolve a local wall clock using offsets on both sides of a transition. */
|
|
46
|
+
function occurrence(day, time, zone) {
|
|
47
|
+
const [hour, minute] = time.split(':').map(Number);
|
|
48
|
+
const wall = day + hour * 3_600_000 + minute * 60_000;
|
|
49
|
+
const offsets = new Set();
|
|
50
|
+
for (let hours = -36; hours <= 36; hours += 12) {
|
|
51
|
+
const at = wall + hours * 3_600_000;
|
|
52
|
+
offsets.add(localWall(at, zone) - at);
|
|
53
|
+
}
|
|
54
|
+
const candidates = [...offsets].map((offset) => wall - offset);
|
|
55
|
+
const exact = candidates.filter((at) => localWall(at, zone) === wall);
|
|
56
|
+
// A fold chooses the first instant. A gap advances by the offset gap, retaining minutes.
|
|
57
|
+
if (exact.length)
|
|
58
|
+
return Math.min(...exact);
|
|
59
|
+
return candidates
|
|
60
|
+
.filter((at) => localWall(at, zone) > wall)
|
|
61
|
+
.sort((a, b) => localWall(a, zone) - localWall(b, zone) || a - b)[0];
|
|
62
|
+
}
|
|
63
|
+
/** A tool's date/time is local to the deployment, never the host's Date parser. */
|
|
64
|
+
export function localRoutineInstant(value, zone) {
|
|
65
|
+
const match = /^(\d{4})-(\d{2})-(\d{2})T((?:[01]\d|2[0-3]):[0-5]\d)$/.exec(value);
|
|
66
|
+
if (!match)
|
|
67
|
+
throw new Error('Invalid local date/time; use YYYY-MM-DDTHH:mm');
|
|
68
|
+
const [, year, month, date, time] = match;
|
|
69
|
+
const day = Date.UTC(Number(year), Number(month) - 1, Number(date));
|
|
70
|
+
if (new Date(day).toISOString().slice(0, 10) !== value.slice(0, 10))
|
|
71
|
+
throw new Error('Invalid local calendar date');
|
|
72
|
+
return new Date(occurrence(day, time, zone)).toISOString();
|
|
73
|
+
}
|
|
74
|
+
export function routineLocalTime(at, zone) {
|
|
75
|
+
return new Date(localWall(at, zone))
|
|
76
|
+
.toISOString()
|
|
77
|
+
.slice(0, 16)
|
|
78
|
+
.replace('T', ' ');
|
|
79
|
+
}
|
|
80
|
+
function matches(day, schedule) {
|
|
81
|
+
return (!schedule.days ||
|
|
82
|
+
schedule.days.includes(weekdays[new Date(day).getUTCDay()]));
|
|
83
|
+
}
|
|
84
|
+
export function nextOccurrence(schedule, after, zone) {
|
|
85
|
+
if ('at' in schedule) {
|
|
86
|
+
const at = Date.parse(schedule.at);
|
|
87
|
+
return at > after ? at : undefined;
|
|
88
|
+
}
|
|
89
|
+
const start = localDay(after, zone);
|
|
90
|
+
for (let i = 0; i <= 8; i++) {
|
|
91
|
+
const day = start + i * 86_400_000;
|
|
92
|
+
if (!matches(day, schedule))
|
|
93
|
+
continue;
|
|
94
|
+
const at = occurrence(day, schedule.time, zone);
|
|
95
|
+
if (at > after)
|
|
96
|
+
return at;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
export function latestOccurrence(schedule, after, through, zone) {
|
|
100
|
+
if ('at' in schedule) {
|
|
101
|
+
const at = Date.parse(schedule.at);
|
|
102
|
+
return at > after && at <= through ? at : undefined;
|
|
103
|
+
}
|
|
104
|
+
const start = localDay(through, zone);
|
|
105
|
+
for (let i = 0; i <= 8; i++) {
|
|
106
|
+
const day = start - i * 86_400_000;
|
|
107
|
+
if (!matches(day, schedule))
|
|
108
|
+
continue;
|
|
109
|
+
const at = occurrence(day, schedule.time, zone);
|
|
110
|
+
if (at > after && at <= through)
|
|
111
|
+
return at;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
//# sourceMappingURL=routine-time.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routine-time.js","sourceRoot":"","sources":["../src/routine-time.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
import type { VelaLogger, VelaSession } from '@glows777/vela-core/extension-api';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import type { ChannelDirectory } from './directory.ts';
|
|
4
|
+
import type { ChannelDefinition, ChannelRoutine, ChannelRuntime, IncomingMessage } from './types.ts';
|
|
5
|
+
/** Provider input must have an object root; the pipe retains strict operation validation. */
|
|
6
|
+
export declare const routineToolSchema: z.ZodPipe<z.ZodObject<{
|
|
7
|
+
message: z.ZodOptional<z.ZodString>;
|
|
8
|
+
schedule: z.ZodOptional<z.ZodUnion<readonly [z.ZodObject<{
|
|
9
|
+
at: z.ZodString;
|
|
10
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
11
|
+
time: z.ZodString;
|
|
12
|
+
days: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
13
|
+
mon: "mon";
|
|
14
|
+
tue: "tue";
|
|
15
|
+
wed: "wed";
|
|
16
|
+
thu: "thu";
|
|
17
|
+
fri: "fri";
|
|
18
|
+
sat: "sat";
|
|
19
|
+
sun: "sun";
|
|
20
|
+
}>>>;
|
|
21
|
+
}, z.core.$strict>]>>;
|
|
22
|
+
action: z.ZodEnum<{
|
|
23
|
+
create: "create";
|
|
24
|
+
list: "list";
|
|
25
|
+
cancel: "cancel";
|
|
26
|
+
}>;
|
|
27
|
+
id: z.ZodOptional<z.ZodString>;
|
|
28
|
+
}, z.core.$strict>, z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
29
|
+
action: z.ZodLiteral<"create">;
|
|
30
|
+
message: z.ZodString;
|
|
31
|
+
schedule: z.ZodUnion<readonly [z.ZodObject<{
|
|
32
|
+
at: z.ZodString;
|
|
33
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
34
|
+
time: z.ZodString;
|
|
35
|
+
days: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
36
|
+
mon: "mon";
|
|
37
|
+
tue: "tue";
|
|
38
|
+
wed: "wed";
|
|
39
|
+
thu: "thu";
|
|
40
|
+
fri: "fri";
|
|
41
|
+
sat: "sat";
|
|
42
|
+
sun: "sun";
|
|
43
|
+
}>>>;
|
|
44
|
+
}, z.core.$strict>]>;
|
|
45
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
46
|
+
action: z.ZodLiteral<"list">;
|
|
47
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
48
|
+
action: z.ZodLiteral<"cancel">;
|
|
49
|
+
id: z.ZodString;
|
|
50
|
+
}, z.core.$strict>], "action">>;
|
|
51
|
+
export declare const channelRoutinesSchema: z.ZodObject<{
|
|
52
|
+
version: z.ZodLiteral<1>;
|
|
53
|
+
routines: z.ZodArray<z.ZodObject<{
|
|
54
|
+
id: z.ZodString;
|
|
55
|
+
channel: z.ZodString;
|
|
56
|
+
chatId: z.ZodString;
|
|
57
|
+
targetKey: z.ZodString;
|
|
58
|
+
creator: z.ZodObject<{
|
|
59
|
+
id: z.ZodString;
|
|
60
|
+
name: z.ZodString;
|
|
61
|
+
}, z.core.$strip>;
|
|
62
|
+
message: z.ZodString;
|
|
63
|
+
schedule: z.ZodUnion<readonly [z.ZodObject<{
|
|
64
|
+
at: z.ZodISODateTime;
|
|
65
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
66
|
+
time: z.ZodString;
|
|
67
|
+
days: z.ZodOptional<z.ZodArray<z.ZodEnum<{
|
|
68
|
+
mon: "mon";
|
|
69
|
+
tue: "tue";
|
|
70
|
+
wed: "wed";
|
|
71
|
+
thu: "thu";
|
|
72
|
+
fri: "fri";
|
|
73
|
+
sat: "sat";
|
|
74
|
+
sun: "sun";
|
|
75
|
+
}>>>;
|
|
76
|
+
}, z.core.$strict>]>;
|
|
77
|
+
createdAt: z.ZodISODateTime;
|
|
78
|
+
lastFiredAt: z.ZodNullable<z.ZodISODateTime>;
|
|
79
|
+
}, z.core.$strict>>;
|
|
80
|
+
}, z.core.$strict>;
|
|
81
|
+
export interface RoutineClock {
|
|
82
|
+
now(): number;
|
|
83
|
+
setTimeout(callback: () => void, delay: number): unknown;
|
|
84
|
+
clearTimeout(timer: unknown): void;
|
|
85
|
+
}
|
|
86
|
+
/** Channel-local persistence and wake-ups. Gateway queues own execution and recovery. */
|
|
87
|
+
export declare class ChannelRoutines {
|
|
88
|
+
private directory;
|
|
89
|
+
private channel;
|
|
90
|
+
private enqueue;
|
|
91
|
+
private restored;
|
|
92
|
+
private logger;
|
|
93
|
+
clock: RoutineClock;
|
|
94
|
+
private records;
|
|
95
|
+
private writing;
|
|
96
|
+
private loaded?;
|
|
97
|
+
private zones;
|
|
98
|
+
private started;
|
|
99
|
+
private disabled;
|
|
100
|
+
private inFlight;
|
|
101
|
+
readonly active: Set<Promise<void>>;
|
|
102
|
+
private timer?;
|
|
103
|
+
private stopped;
|
|
104
|
+
private readonly path?;
|
|
105
|
+
constructor(dataDir: string | undefined, directory: ChannelDirectory, channel: (name: string) => ChannelDefinition | undefined, enqueue: (name: string, msg: IncomingMessage) => Promise<void>, restored: (key: string, name: string, msg: IncomingMessage) => Promise<VelaSession | undefined>, logger: VelaLogger);
|
|
106
|
+
load(channels: Iterable<ChannelDefinition>): Promise<void>;
|
|
107
|
+
private read;
|
|
108
|
+
start(name: string): void;
|
|
109
|
+
private change;
|
|
110
|
+
list(channel: string, chatId: string): ChannelRoutine[];
|
|
111
|
+
create(input: Omit<ChannelRoutine, 'id' | 'createdAt' | 'lastFiredAt'>): Promise<ChannelRoutine>;
|
|
112
|
+
cancel(channel: string, chatId: string, id: string): Promise<boolean>;
|
|
113
|
+
private describe;
|
|
114
|
+
private describeChat;
|
|
115
|
+
formatList(channel: string, chatId: string): string;
|
|
116
|
+
scoped(name: string, context: (id: string) => IncomingMessage | undefined): ChannelRuntime['routines'];
|
|
117
|
+
confirmed(msg: IncomingMessage): Promise<void>;
|
|
118
|
+
/** Re-align a committed session marker before submitting another scheduled input. */
|
|
119
|
+
shouldRun(msg: IncomingMessage, session: VelaSession): Promise<boolean>;
|
|
120
|
+
private tick;
|
|
121
|
+
private deliver;
|
|
122
|
+
private arm;
|
|
123
|
+
stop(): void;
|
|
124
|
+
drain(): Promise<void>;
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=routines.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"routines.d.ts","sourceRoot":"","sources":["../src/routines.ts"],"names":[],"mappings":"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"}
|
package/dist/routines.js
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
import { readFile } from 'node:fs/promises';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import { z } from 'zod';
|
|
4
|
+
import { writeAtomicJson } from "./atomic-json.js";
|
|
5
|
+
import { errorMessage } from "./log.js";
|
|
6
|
+
import { latestOccurrence, localRoutineInstant, nextOccurrence, routineLocalTime, routineTimezone, } from "./routine-time.js";
|
|
7
|
+
const instant = z.iso
|
|
8
|
+
.datetime()
|
|
9
|
+
.refine((value) => Number.isFinite(Date.parse(value)), 'Invalid instant');
|
|
10
|
+
const recurringSchema = z
|
|
11
|
+
.object({
|
|
12
|
+
time: z.string().regex(/^(?:[01]\d|2[0-3]):[0-5]\d$/),
|
|
13
|
+
days: z
|
|
14
|
+
.array(z.enum(['mon', 'tue', 'wed', 'thu', 'fri', 'sat', 'sun']))
|
|
15
|
+
.min(1)
|
|
16
|
+
.optional(),
|
|
17
|
+
})
|
|
18
|
+
.strict();
|
|
19
|
+
const scheduleSchema = z.union([
|
|
20
|
+
z.object({ at: instant }).strict(),
|
|
21
|
+
recurringSchema,
|
|
22
|
+
]);
|
|
23
|
+
const createRoutineSchema = z
|
|
24
|
+
.object({
|
|
25
|
+
action: z.literal('create'),
|
|
26
|
+
message: z.string().trim().min(1),
|
|
27
|
+
schedule: z.union([z.object({ at: z.string() }).strict(), recurringSchema]),
|
|
28
|
+
})
|
|
29
|
+
.strict();
|
|
30
|
+
/** Provider input must have an object root; the pipe retains strict operation validation. */
|
|
31
|
+
export const routineToolSchema = createRoutineSchema
|
|
32
|
+
.extend({
|
|
33
|
+
action: z.enum(['create', 'list', 'cancel']),
|
|
34
|
+
id: z.string().min(1).optional(),
|
|
35
|
+
})
|
|
36
|
+
.partial({ message: true, schedule: true })
|
|
37
|
+
.pipe(z.discriminatedUnion('action', [
|
|
38
|
+
createRoutineSchema,
|
|
39
|
+
z.object({ action: z.literal('list') }).strict(),
|
|
40
|
+
z.object({ action: z.literal('cancel'), id: z.string().min(1) }).strict(),
|
|
41
|
+
]));
|
|
42
|
+
const routineSchema = z
|
|
43
|
+
.object({
|
|
44
|
+
id: z.string().min(1),
|
|
45
|
+
channel: z.string().min(1),
|
|
46
|
+
chatId: z.string().min(1),
|
|
47
|
+
targetKey: z.string().min(1),
|
|
48
|
+
creator: z.object({ id: z.string().min(1), name: z.string().min(1) }),
|
|
49
|
+
message: z.string().trim().min(1),
|
|
50
|
+
schedule: scheduleSchema,
|
|
51
|
+
createdAt: instant,
|
|
52
|
+
lastFiredAt: instant.nullable(),
|
|
53
|
+
})
|
|
54
|
+
.strict();
|
|
55
|
+
export const channelRoutinesSchema = z
|
|
56
|
+
.object({ version: z.literal(1), routines: z.array(routineSchema) })
|
|
57
|
+
.strict();
|
|
58
|
+
/** Channel-local persistence and wake-ups. Gateway queues own execution and recovery. */
|
|
59
|
+
export class ChannelRoutines {
|
|
60
|
+
directory;
|
|
61
|
+
channel;
|
|
62
|
+
enqueue;
|
|
63
|
+
restored;
|
|
64
|
+
logger;
|
|
65
|
+
clock = {
|
|
66
|
+
now: Date.now,
|
|
67
|
+
setTimeout: (callback, delay) => setTimeout(callback, delay),
|
|
68
|
+
clearTimeout: (timer) => clearTimeout(timer),
|
|
69
|
+
};
|
|
70
|
+
records = [];
|
|
71
|
+
writing = Promise.resolve();
|
|
72
|
+
loaded;
|
|
73
|
+
zones = new Map();
|
|
74
|
+
started = new Set();
|
|
75
|
+
disabled = new Set();
|
|
76
|
+
inFlight = new Map();
|
|
77
|
+
active = new Set();
|
|
78
|
+
timer;
|
|
79
|
+
stopped = true;
|
|
80
|
+
path;
|
|
81
|
+
constructor(dataDir, directory, channel, enqueue, restored, logger) {
|
|
82
|
+
this.directory = directory;
|
|
83
|
+
this.channel = channel;
|
|
84
|
+
this.enqueue = enqueue;
|
|
85
|
+
this.restored = restored;
|
|
86
|
+
this.logger = logger;
|
|
87
|
+
this.path = dataDir ? join(dataDir, 'channels', 'routines.json') : undefined;
|
|
88
|
+
}
|
|
89
|
+
async load(channels) {
|
|
90
|
+
for (const ch of channels)
|
|
91
|
+
this.zones.set(ch.name, routineTimezone(ch.timezone));
|
|
92
|
+
this.loaded ??= this.read();
|
|
93
|
+
await this.loaded;
|
|
94
|
+
this.started.clear();
|
|
95
|
+
this.disabled.clear();
|
|
96
|
+
this.stopped = false;
|
|
97
|
+
}
|
|
98
|
+
async read() {
|
|
99
|
+
if (!this.path)
|
|
100
|
+
return;
|
|
101
|
+
let text;
|
|
102
|
+
try {
|
|
103
|
+
text = await readFile(this.path, 'utf8');
|
|
104
|
+
}
|
|
105
|
+
catch (error) {
|
|
106
|
+
if (error.code === 'ENOENT')
|
|
107
|
+
return;
|
|
108
|
+
throw new Error(`Cannot read channel routines ${this.path}`, {
|
|
109
|
+
cause: error,
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
const data = channelRoutinesSchema.parse(JSON.parse(text));
|
|
114
|
+
if (new Set(data.routines.map((r) => r.id)).size !== data.routines.length)
|
|
115
|
+
throw new Error('Duplicate routine id');
|
|
116
|
+
this.records = data.routines;
|
|
117
|
+
}
|
|
118
|
+
catch (cause) {
|
|
119
|
+
throw new Error(`Invalid channel routines ${this.path}: ${String(cause)}`, { cause });
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
start(name) {
|
|
123
|
+
this.started.add(name);
|
|
124
|
+
this.tick(name);
|
|
125
|
+
}
|
|
126
|
+
change(update) {
|
|
127
|
+
const task = this.writing
|
|
128
|
+
.catch(() => { })
|
|
129
|
+
.then(async () => {
|
|
130
|
+
const next = update(this.records);
|
|
131
|
+
if (this.path)
|
|
132
|
+
await writeAtomicJson(this.path, { version: 1, routines: next });
|
|
133
|
+
this.records = next;
|
|
134
|
+
});
|
|
135
|
+
this.writing = task;
|
|
136
|
+
this.active.add(task);
|
|
137
|
+
void task.then(() => this.active.delete(task), () => this.active.delete(task));
|
|
138
|
+
return task;
|
|
139
|
+
}
|
|
140
|
+
list(channel, chatId) {
|
|
141
|
+
return structuredClone(this.records.filter((r) => r.channel === channel && r.chatId === chatId));
|
|
142
|
+
}
|
|
143
|
+
async create(input) {
|
|
144
|
+
const record = routineSchema.parse({
|
|
145
|
+
...input,
|
|
146
|
+
id: crypto.randomUUID(),
|
|
147
|
+
createdAt: new Date(this.clock.now()).toISOString(),
|
|
148
|
+
lastFiredAt: null,
|
|
149
|
+
});
|
|
150
|
+
if (!nextOccurrence(record.schedule, this.clock.now(), this.zones.get(record.channel)))
|
|
151
|
+
throw new Error('A one-off routine must be in the future');
|
|
152
|
+
await this.change((records) => {
|
|
153
|
+
if (records.filter((r) => r.channel === record.channel && r.chatId === record.chatId).length >= 20)
|
|
154
|
+
throw new Error('This chat already has 20 routines');
|
|
155
|
+
return [...records, record];
|
|
156
|
+
});
|
|
157
|
+
this.arm();
|
|
158
|
+
return structuredClone(record);
|
|
159
|
+
}
|
|
160
|
+
async cancel(channel, chatId, id) {
|
|
161
|
+
let cancelled = false;
|
|
162
|
+
await this.change((records) => records.filter((r) => {
|
|
163
|
+
const matches = r.id === id && r.channel === channel && r.chatId === chatId;
|
|
164
|
+
if (matches)
|
|
165
|
+
cancelled = true;
|
|
166
|
+
return !matches;
|
|
167
|
+
}));
|
|
168
|
+
this.arm();
|
|
169
|
+
return cancelled;
|
|
170
|
+
}
|
|
171
|
+
describe(record) {
|
|
172
|
+
const timezone = this.zones.get(record.channel);
|
|
173
|
+
const target = this.directory.get(record.targetKey);
|
|
174
|
+
const next = nextOccurrence(record.schedule, this.clock.now(), timezone);
|
|
175
|
+
return {
|
|
176
|
+
...structuredClone(record),
|
|
177
|
+
timezone,
|
|
178
|
+
scheduleText: 'at' in record.schedule
|
|
179
|
+
? routineLocalTime(Date.parse(record.schedule.at), timezone)
|
|
180
|
+
: `${record.schedule.days?.join(',') ?? 'daily'} at ${record.schedule.time}`,
|
|
181
|
+
target: !target
|
|
182
|
+
? 'Unavailable target'
|
|
183
|
+
: target.rootId
|
|
184
|
+
? `Topic: ${target.task}`
|
|
185
|
+
: target.chatKind === 'group'
|
|
186
|
+
? 'Group channel'
|
|
187
|
+
: 'Direct chat',
|
|
188
|
+
nextRun: next === undefined ? null : routineLocalTime(next, timezone),
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
describeChat(channel, chatId) {
|
|
192
|
+
return {
|
|
193
|
+
timezone: this.zones.get(channel),
|
|
194
|
+
routines: this.list(channel, chatId).map((r) => this.describe(r)),
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
formatList(channel, chatId) {
|
|
198
|
+
const { timezone, routines } = this.describeChat(channel, chatId);
|
|
199
|
+
return [
|
|
200
|
+
`Routines (${timezone})`,
|
|
201
|
+
...routines.map((r) => `${r.id} | ${r.scheduleText} | ${r.target} | set by ${r.creator.name} | next: ${r.nextRun ?? 'no future occurrence'}`),
|
|
202
|
+
...(routines.length ? [] : ['No routines in this chat.']),
|
|
203
|
+
].join('\n');
|
|
204
|
+
}
|
|
205
|
+
scoped(name, context) {
|
|
206
|
+
const source = (id) => {
|
|
207
|
+
const msg = context(id);
|
|
208
|
+
const key = this.directory.keyForSession(id);
|
|
209
|
+
if (!msg || this.directory.sessionId(key) !== id)
|
|
210
|
+
throw new Error('Routines require an active channel run');
|
|
211
|
+
return { msg, key };
|
|
212
|
+
};
|
|
213
|
+
return {
|
|
214
|
+
create: async (id, message, schedule) => {
|
|
215
|
+
const { msg, key } = source(id);
|
|
216
|
+
const record = await this.create({
|
|
217
|
+
channel: name,
|
|
218
|
+
chatId: msg.channelId,
|
|
219
|
+
targetKey: key,
|
|
220
|
+
creator: { id: msg.senderId, name: msg.senderName },
|
|
221
|
+
message,
|
|
222
|
+
schedule: 'at' in schedule
|
|
223
|
+
? { at: localRoutineInstant(schedule.at, this.zones.get(name)) }
|
|
224
|
+
: schedule,
|
|
225
|
+
});
|
|
226
|
+
return this.describe(record);
|
|
227
|
+
},
|
|
228
|
+
list: (id) => this.describeChat(name, source(id).msg.channelId),
|
|
229
|
+
cancel: (id, routineId) => this.cancel(name, source(id).msg.channelId, routineId),
|
|
230
|
+
};
|
|
231
|
+
}
|
|
232
|
+
async confirmed(msg) {
|
|
233
|
+
if (!msg.routine)
|
|
234
|
+
return;
|
|
235
|
+
const { id, scheduledAt } = msg.routine;
|
|
236
|
+
await this.change((records) => records.flatMap((r) => {
|
|
237
|
+
if (r.id !== id ||
|
|
238
|
+
r.channel !== this.directory.get(r.targetKey)?.channel ||
|
|
239
|
+
r.chatId !== msg.channelId ||
|
|
240
|
+
(r.lastFiredAt &&
|
|
241
|
+
Date.parse(r.lastFiredAt) >= Date.parse(scheduledAt)))
|
|
242
|
+
return [r];
|
|
243
|
+
return 'at' in r.schedule ? [] : [{ ...r, lastFiredAt: scheduledAt }];
|
|
244
|
+
}));
|
|
245
|
+
this.arm();
|
|
246
|
+
}
|
|
247
|
+
/** Re-align a committed session marker before submitting another scheduled input. */
|
|
248
|
+
async shouldRun(msg, session) {
|
|
249
|
+
for (const entry of session.getEntries()) {
|
|
250
|
+
if (entry.type !== 'custom' || entry.customType !== 'channel.input')
|
|
251
|
+
continue;
|
|
252
|
+
const routine = entry.data
|
|
253
|
+
?.routine;
|
|
254
|
+
if (routine?.id === msg.routine?.id)
|
|
255
|
+
await this.confirmed({ ...msg, routine });
|
|
256
|
+
}
|
|
257
|
+
const record = this.records.find((r) => r.id === msg.routine?.id);
|
|
258
|
+
return (!!record &&
|
|
259
|
+
(!record.lastFiredAt ||
|
|
260
|
+
Date.parse(record.lastFiredAt) < Date.parse(msg.routine.scheduledAt)));
|
|
261
|
+
}
|
|
262
|
+
tick(name) {
|
|
263
|
+
if (this.stopped)
|
|
264
|
+
return;
|
|
265
|
+
const now = this.clock.now();
|
|
266
|
+
for (const record of this.records) {
|
|
267
|
+
if ((name && name !== record.channel) ||
|
|
268
|
+
!this.started.has(record.channel) ||
|
|
269
|
+
(this.inFlight.has(record.id) &&
|
|
270
|
+
Date.parse(record.lastFiredAt ?? record.createdAt) <
|
|
271
|
+
this.inFlight.get(record.id)))
|
|
272
|
+
continue;
|
|
273
|
+
const at = latestOccurrence(record.schedule, Date.parse(record.lastFiredAt ?? record.createdAt), now, this.zones.get(record.channel));
|
|
274
|
+
if (at === undefined)
|
|
275
|
+
continue;
|
|
276
|
+
this.inFlight.set(record.id, at);
|
|
277
|
+
const task = this.deliver(record, at, !!name)
|
|
278
|
+
.catch((error) => {
|
|
279
|
+
this.logger.error(`[${record.channel}] Routine ${record.id} failed: ${errorMessage(error)}`);
|
|
280
|
+
})
|
|
281
|
+
.finally(() => {
|
|
282
|
+
if (this.inFlight.get(record.id) === at)
|
|
283
|
+
this.inFlight.delete(record.id);
|
|
284
|
+
this.active.delete(task);
|
|
285
|
+
this.arm();
|
|
286
|
+
});
|
|
287
|
+
this.active.add(task);
|
|
288
|
+
}
|
|
289
|
+
this.arm();
|
|
290
|
+
}
|
|
291
|
+
async deliver(routine, at, startup) {
|
|
292
|
+
const record = this.directory.get(routine.targetKey);
|
|
293
|
+
const channel = this.channel(routine.channel);
|
|
294
|
+
if (!record ||
|
|
295
|
+
record.channel !== routine.channel ||
|
|
296
|
+
record.chatId !== routine.chatId ||
|
|
297
|
+
!channel) {
|
|
298
|
+
this.logger.warn(`[${routine.channel}] Routine ${routine.id} has no logical target`);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
const late = this.clock.now() - at;
|
|
302
|
+
let msg = {
|
|
303
|
+
channelId: routine.chatId,
|
|
304
|
+
senderId: routine.creator.id,
|
|
305
|
+
senderName: routine.creator.name,
|
|
306
|
+
text: `Scheduled wake-up ${routine.id}, set by ${routine.creator.name}, scheduled for ${new Date(at).toISOString()} (${this.zones.get(routine.channel)})${late > 0 ? `; late by ${Math.ceil(late / 1000)} seconds` : ''}:\n${routine.message}`,
|
|
307
|
+
routine: { id: routine.id, scheduledAt: new Date(at).toISOString() },
|
|
308
|
+
...(record.rootId && record.threadId
|
|
309
|
+
? {
|
|
310
|
+
topic: { rootId: record.rootId, threadId: record.threadId },
|
|
311
|
+
replyToId: record.rootId,
|
|
312
|
+
}
|
|
313
|
+
: {}),
|
|
314
|
+
};
|
|
315
|
+
if (channel.canRecover?.(msg, record.chatKind) === false) {
|
|
316
|
+
const chat = `${routine.channel}\0${routine.chatId}`;
|
|
317
|
+
if (!this.disabled.has(chat)) {
|
|
318
|
+
this.disabled.add(chat);
|
|
319
|
+
this.logger.info(`[${routine.channel}] Routine skipped: chat is disabled`);
|
|
320
|
+
}
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (this.stopped || !this.records.some((r) => r.id === routine.id))
|
|
324
|
+
return;
|
|
325
|
+
if (startup) {
|
|
326
|
+
// Recovery owns the key first. Reconcile saved inputs before announcing a catch-up.
|
|
327
|
+
const session = await this.restored(routine.targetKey, routine.channel, msg);
|
|
328
|
+
if (!session || !(await this.shouldRun(msg, session)) || this.stopped)
|
|
329
|
+
return;
|
|
330
|
+
}
|
|
331
|
+
if (channel.prepareRoutine) {
|
|
332
|
+
const prepared = await channel.prepareRoutine(msg, record.chatKind, {
|
|
333
|
+
message: routine.message,
|
|
334
|
+
lateByMs: late,
|
|
335
|
+
});
|
|
336
|
+
if (!prepared)
|
|
337
|
+
return;
|
|
338
|
+
msg = prepared;
|
|
339
|
+
}
|
|
340
|
+
if (!this.stopped)
|
|
341
|
+
await this.enqueue(routine.channel, msg);
|
|
342
|
+
}
|
|
343
|
+
arm() {
|
|
344
|
+
this.clock.clearTimeout(this.timer);
|
|
345
|
+
this.timer = undefined;
|
|
346
|
+
if (this.stopped)
|
|
347
|
+
return;
|
|
348
|
+
const now = this.clock.now();
|
|
349
|
+
const times = this.records.flatMap((r) => {
|
|
350
|
+
if (!this.started.has(r.channel))
|
|
351
|
+
return [];
|
|
352
|
+
const next = nextOccurrence(r.schedule, Math.max(now, Date.parse(r.lastFiredAt ?? r.createdAt)), this.zones.get(r.channel));
|
|
353
|
+
return next === undefined ? [] : [next];
|
|
354
|
+
});
|
|
355
|
+
if (!times.length)
|
|
356
|
+
return;
|
|
357
|
+
this.timer = this.clock.setTimeout(() => this.tick(), Math.min(2_147_483_647, Math.max(0, Math.min(...times) - now)));
|
|
358
|
+
}
|
|
359
|
+
stop() {
|
|
360
|
+
this.stopped = true;
|
|
361
|
+
this.clock.clearTimeout(this.timer);
|
|
362
|
+
this.timer = undefined;
|
|
363
|
+
}
|
|
364
|
+
async drain() {
|
|
365
|
+
while (this.active.size)
|
|
366
|
+
await Promise.all([...this.active]);
|
|
367
|
+
await this.writing;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
//# sourceMappingURL=routines.js.map
|