@frockbot/plugin-routines 0.3.45 → 0.3.47

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.
@@ -0,0 +1,185 @@
1
+ import { canonicalCommandFingerprintV1 } from "@frockbot/configuration-core";
2
+ import {
3
+ decodeRoutineSubscriptionIntentV1,
4
+ type RoutineSubscriptionIntentV1,
5
+ } from "@frockbot/connection-core";
6
+ import type { RoutineRecordV1 } from "./records.js";
7
+ import type {
8
+ RoutineStorageReadsV1,
9
+ RoutineStorageWritesV1,
10
+ RoutineStorageV1,
11
+ } from "./store.js";
12
+
13
+ const PREFIX = "routine-subscription:",
14
+ POINTER = "routine-subscription-current:";
15
+ interface Binding {
16
+ schemaVersion: 1;
17
+ intent: RoutineSubscriptionIntentV1;
18
+ pending: boolean;
19
+ dueAt: number;
20
+ }
21
+ function decode(value: unknown): Binding {
22
+ if (
23
+ !value ||
24
+ typeof value !== "object" ||
25
+ !("schemaVersion" in value) ||
26
+ value.schemaVersion !== 1 ||
27
+ !("intent" in value) ||
28
+ !("pending" in value) ||
29
+ typeof value.pending !== "boolean" ||
30
+ !("dueAt" in value) ||
31
+ typeof value.dueAt !== "number" ||
32
+ !Number.isFinite(value.dueAt)
33
+ )
34
+ throw new Error("Routine event binding is invalid");
35
+ return {
36
+ schemaVersion: 1,
37
+ intent: decodeRoutineSubscriptionIntentV1(value.intent),
38
+ pending: value.pending,
39
+ dueAt: value.dueAt,
40
+ };
41
+ }
42
+ /** Staged in the same Bot transaction as the Routine; no provider call occurs here. */
43
+ export async function stageRoutineSubscriptionV1(
44
+ tx: RoutineStorageWritesV1,
45
+ routineId: string,
46
+ next?: RoutineRecordV1,
47
+ force = false,
48
+ ) {
49
+ const current = await tx.get<string>(`${POINTER}${routineId}`);
50
+ const held = current
51
+ ? decode(await tx.get(`${PREFIX}${current}`))
52
+ : undefined;
53
+ const trigger =
54
+ next?.trigger?.kind === "connection"
55
+ ? {
56
+ connectionId: next.trigger.connectionId,
57
+ triggerType: next.trigger.triggerType,
58
+ config: next.trigger.config,
59
+ }
60
+ : undefined;
61
+ const same =
62
+ held &&
63
+ trigger &&
64
+ canonicalCommandFingerprintV1("trigger", held.intent.trigger) ===
65
+ canonicalCommandFingerprintV1("trigger", trigger);
66
+ if (same && held) {
67
+ if (held.intent.enabled === next!.enabled && !force) return;
68
+ await tx.put(`${PREFIX}${current}`, {
69
+ schemaVersion: 1,
70
+ intent: {
71
+ ...held.intent,
72
+ revision: held.intent.revision + 1,
73
+ enabled: next!.enabled,
74
+ },
75
+ pending: true,
76
+ dueAt: Date.now(),
77
+ } satisfies Binding);
78
+ return;
79
+ }
80
+ if (held && !held.intent.deleted) {
81
+ await tx.put(`${PREFIX}${current}`, {
82
+ schemaVersion: 1,
83
+ intent: {
84
+ ...held.intent,
85
+ revision: held.intent.revision + 1,
86
+ enabled: false,
87
+ deleted: true,
88
+ },
89
+ pending: true,
90
+ dueAt: Date.now(),
91
+ } satisfies Binding);
92
+ await tx.delete(`${POINTER}${routineId}`);
93
+ }
94
+ if (!trigger) return;
95
+ if ((await tx.list({ prefix: PREFIX, limit: 1000 })).size >= 1000)
96
+ throw new Error(
97
+ "This Bot has reached its event subscription history limit",
98
+ );
99
+ const subscriptionId = crypto.randomUUID();
100
+ await tx.put(`${PREFIX}${subscriptionId}`, {
101
+ schemaVersion: 1,
102
+ intent: {
103
+ schemaVersion: 1,
104
+ subscriptionId,
105
+ routineId,
106
+ revision: 1,
107
+ enabled: next!.enabled,
108
+ deleted: false,
109
+ trigger,
110
+ },
111
+ pending: true,
112
+ dueAt: Date.now(),
113
+ } satisfies Binding);
114
+ await tx.put(`${POINTER}${routineId}`, subscriptionId);
115
+ }
116
+ export async function routineSubscriptionMatchesV1(
117
+ tx: RoutineStorageReadsV1,
118
+ routineId: string,
119
+ subscriptionId: string,
120
+ ): Promise<boolean> {
121
+ if ((await tx.get(`${POINTER}${routineId}`)) !== subscriptionId) return false;
122
+ const binding = decode(await tx.get(`${PREFIX}${subscriptionId}`));
123
+ return binding.intent.enabled && !binding.intent.deleted;
124
+ }
125
+ export async function routineSubscriptionDeadlinesV1(
126
+ tx: RoutineStorageReadsV1,
127
+ ): Promise<number[]> {
128
+ return [...(await tx.list({ prefix: PREFIX })).values()]
129
+ .map(decode)
130
+ .filter((binding) => binding.pending)
131
+ .map((binding) => binding.dueAt);
132
+ }
133
+ export async function flushRoutineSubscriptionsV1(
134
+ storage: RoutineStorageV1,
135
+ send: (intent: RoutineSubscriptionIntentV1) => Promise<unknown>,
136
+ force = false,
137
+ ): Promise<void> {
138
+ for (const [key, value] of await storage.list({ prefix: PREFIX })) {
139
+ const binding = decode(value);
140
+ if (!binding.pending || (!force && binding.dueAt > Date.now())) continue;
141
+ await storage.transaction(async (tx) => {
142
+ const live = decode(await tx.get(key));
143
+ if (live.intent.revision === binding.intent.revision)
144
+ await tx.put(key, { ...live, dueAt: Date.now() + 60_000 });
145
+ });
146
+ try {
147
+ const receipt = await send(binding.intent);
148
+ if (
149
+ !receipt ||
150
+ typeof receipt !== "object" ||
151
+ !("schemaVersion" in receipt) ||
152
+ receipt.schemaVersion !== 1 ||
153
+ !("status" in receipt) ||
154
+ receipt.status !== "recorded"
155
+ )
156
+ throw new Error("Routine subscription was not acknowledged");
157
+ await storage.transaction(async (tx) => {
158
+ const live = decode(await tx.get(key));
159
+ if (live.intent.revision === binding.intent.revision)
160
+ await tx.put(key, { ...live, pending: false });
161
+ });
162
+ } catch {
163
+ /* The persisted deadline retries the same desired revision. */
164
+ }
165
+ }
166
+ }
167
+
168
+ export async function routineSubscriptionBindingsV1(
169
+ storage: RoutineStorageReadsV1,
170
+ ): Promise<Record<string, string>> {
171
+ const result: Record<string, string> = {};
172
+ for (const [key, id] of await storage.list<unknown>({ prefix: POINTER })) {
173
+ if (typeof id !== "string")
174
+ throw new Error("Routine event binding is invalid");
175
+ const held = decode(await storage.get(`${PREFIX}${id}`));
176
+ if (
177
+ held.intent.subscriptionId !== id ||
178
+ held.intent.routineId !== key.slice(POINTER.length) ||
179
+ held.intent.deleted
180
+ )
181
+ throw new Error("Routine event binding is invalid");
182
+ result[held.intent.routineId] = id;
183
+ }
184
+ return result;
185
+ }