solid-objects 0.14.6 → 0.14.7
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 +16 -0
- package/README.md +4 -0
- package/dist/actor-runtime.d.ts +36 -0
- package/dist/actor-runtime.d.ts.map +1 -0
- package/dist/actor-runtime.js +2 -0
- package/dist/actor-runtime.js.map +1 -0
- package/dist/browser/index.js +1 -1
- package/dist/browser/index.js.map +1 -1
- package/dist/cloudflare/configuration.d.ts +32 -0
- package/dist/cloudflare/configuration.d.ts.map +1 -0
- package/dist/cloudflare/configuration.js +46 -0
- package/dist/cloudflare/configuration.js.map +1 -0
- package/dist/cloudflare/engine.d.ts +43 -0
- package/dist/cloudflare/engine.d.ts.map +1 -0
- package/dist/cloudflare/engine.js +921 -0
- package/dist/cloudflare/engine.js.map +1 -0
- package/dist/cloudflare/host.d.ts +10 -0
- package/dist/cloudflare/host.d.ts.map +1 -0
- package/dist/cloudflare/host.js +44 -0
- package/dist/cloudflare/host.js.map +1 -0
- package/dist/cloudflare/index.d.ts +9 -0
- package/dist/cloudflare/index.d.ts.map +1 -0
- package/dist/cloudflare/index.js +8 -0
- package/dist/cloudflare/index.js.map +1 -0
- package/dist/cloudflare/platform.d.ts +2 -0
- package/dist/cloudflare/platform.d.ts.map +1 -0
- package/dist/cloudflare/platform.js +4 -0
- package/dist/cloudflare/platform.js.map +1 -0
- package/dist/cloudflare/protocol.d.ts +58 -0
- package/dist/cloudflare/protocol.d.ts.map +1 -0
- package/dist/cloudflare/protocol.js +77 -0
- package/dist/cloudflare/protocol.js.map +1 -0
- package/dist/cloudflare/records.d.ts +71 -0
- package/dist/cloudflare/records.d.ts.map +1 -0
- package/dist/cloudflare/records.js +2 -0
- package/dist/cloudflare/records.js.map +1 -0
- package/dist/cloudflare/runtime.d.ts +83 -0
- package/dist/cloudflare/runtime.d.ts.map +1 -0
- package/dist/cloudflare/runtime.js +321 -0
- package/dist/cloudflare/runtime.js.map +1 -0
- package/dist/cloudflare/session.d.ts +12 -0
- package/dist/cloudflare/session.d.ts.map +1 -0
- package/dist/cloudflare/session.js +332 -0
- package/dist/cloudflare/session.js.map +1 -0
- package/dist/cloudflare/storage.d.ts +25 -0
- package/dist/cloudflare/storage.d.ts.map +1 -0
- package/dist/cloudflare/storage.js +195 -0
- package/dist/cloudflare/storage.js.map +1 -0
- package/dist/context.d.ts +5 -4
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +3 -0
- package/dist/context.js.map +1 -1
- package/dist/core.d.ts +8 -0
- package/dist/core.d.ts.map +1 -0
- package/dist/core.js +4 -0
- package/dist/core.js.map +1 -0
- package/dist/default-runtime.d.ts +4 -4
- package/dist/default-runtime.d.ts.map +1 -1
- package/dist/default-runtime.js.map +1 -1
- package/dist/errors.d.ts +14 -0
- package/dist/errors.d.ts.map +1 -1
- package/dist/errors.js +9 -0
- package/dist/errors.js.map +1 -1
- package/dist/realtime.d.ts +18 -2
- package/dist/realtime.d.ts.map +1 -1
- package/dist/realtime.js.map +1 -1
- package/dist/reference.d.ts +5 -5
- package/dist/reference.d.ts.map +1 -1
- package/dist/reference.js.map +1 -1
- package/dist/repository.d.ts +5 -1
- package/dist/repository.d.ts.map +1 -1
- package/dist/repository.js +16 -0
- package/dist/repository.js.map +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +18 -53
- package/dist/runtime.js.map +1 -1
- package/dist/turn.d.ts +34 -0
- package/dist/turn.d.ts.map +1 -0
- package/dist/turn.js +57 -0
- package/dist/turn.js.map +1 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/docs/api.md +22 -0
- package/docs/architecture.md +5 -3
- package/docs/cloudflare.md +155 -0
- package/docs/parity.md +19 -0
- package/docs/support.md +12 -11
- package/examples/cloudflare/README.md +49 -0
- package/examples/cloudflare/environment.d.ts +14 -0
- package/examples/cloudflare/shopping-cart.ts +65 -0
- package/examples/cloudflare/worker.ts +87 -0
- package/examples/cloudflare/wrangler.jsonc +15 -0
- package/package.json +20 -5
|
@@ -0,0 +1,921 @@
|
|
|
1
|
+
import { withActorContext, withActorProjection, withRuntime } from "../context.js";
|
|
2
|
+
import { actorState, hydrateActor, initialStateFor, migrateState, validateDefinition, } from "../definition.js";
|
|
3
|
+
import { ActorDestroyed, IdempotencyConflict, MailboxFull, NonRetryableError, PayloadTooLarge, QueryMutatedState, Rejected, ReminderNotPaused, Unauthorized, UnknownActorType, UnknownDeadLetter, UnknownEffect, UnknownOperation, UnknownPayloadBroadcast, UnknownReminder, UnsupportedCapability, } from "../errors.js";
|
|
4
|
+
import { deepCopy, jsonObject, normalizeJson, stableJson } from "../serialization.js";
|
|
5
|
+
import { evaluateActorTurn, readActorObservables, selectActorBroadcast } from "../turn.js";
|
|
6
|
+
import { actorName, callHost } from "./protocol.js";
|
|
7
|
+
import { beforeDeadline, CloudflareRuntime } from "./runtime.js";
|
|
8
|
+
import { ActorStorage } from "./storage.js";
|
|
9
|
+
const RECOVERY_INTERVAL = 30_000;
|
|
10
|
+
export class ActorEngine {
|
|
11
|
+
store;
|
|
12
|
+
runtime;
|
|
13
|
+
definitions = new Map();
|
|
14
|
+
actorRunning = false;
|
|
15
|
+
delivering = new Set();
|
|
16
|
+
cached;
|
|
17
|
+
constructor(store, actors) {
|
|
18
|
+
this.store = store;
|
|
19
|
+
this.runtime = new CloudflareRuntime(store.settings.backend);
|
|
20
|
+
for (const actor of actors) {
|
|
21
|
+
const definition = validateDefinition(actor);
|
|
22
|
+
if (this.definitions.has(definition.type))
|
|
23
|
+
throw new TypeError(`duplicate actor type ${definition.type}`);
|
|
24
|
+
this.definitions.set(definition.type, definition);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
get settings() {
|
|
28
|
+
return this.store.settings;
|
|
29
|
+
}
|
|
30
|
+
async request(input) {
|
|
31
|
+
if (input.method === "enqueue" || input.method === "internal") {
|
|
32
|
+
const operation = String(input.payload.operation);
|
|
33
|
+
if (input.method === "enqueue")
|
|
34
|
+
await this.authorizeOperation(input, {
|
|
35
|
+
operation,
|
|
36
|
+
arguments: jsonObject(input.payload.arguments),
|
|
37
|
+
});
|
|
38
|
+
this.bind(input);
|
|
39
|
+
return this.store.atomic(() => normalizeJson(this.enqueue(input)));
|
|
40
|
+
}
|
|
41
|
+
if (input.method === "message" || input.method === "lookup")
|
|
42
|
+
return this.readMessage(input);
|
|
43
|
+
if (input.method === "administration")
|
|
44
|
+
return this.administer(input);
|
|
45
|
+
if (input.method === "destroy") {
|
|
46
|
+
if (!(await this.settings.authorizeDestroy(input)))
|
|
47
|
+
throw new Unauthorized("actor destruction is not authorized");
|
|
48
|
+
this.bind(input);
|
|
49
|
+
return this.store.atomic(() => this.destroy());
|
|
50
|
+
}
|
|
51
|
+
if (input.method === "unsubscribe") {
|
|
52
|
+
this.bind(input);
|
|
53
|
+
await this.store.atomic(() => {
|
|
54
|
+
this.store.storage.sql.exec("DELETE FROM subscriptions WHERE id = ?", String(input.payload.subscriptionId));
|
|
55
|
+
this.store.storage.sql.exec("DELETE FROM outboxes WHERE kind = 'broadcast' AND destination = ?", String(input.payload.subscriptionId));
|
|
56
|
+
});
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
if (input.method === "snapshot") {
|
|
60
|
+
if (!(await this.settings.authorizeQuery({
|
|
61
|
+
...input,
|
|
62
|
+
operation: "__snapshot__",
|
|
63
|
+
arguments: {},
|
|
64
|
+
})))
|
|
65
|
+
throw new Unauthorized("actor snapshot is not authorized");
|
|
66
|
+
this.bind(input);
|
|
67
|
+
return this.snapshot(input);
|
|
68
|
+
}
|
|
69
|
+
if (!(await this.settings.authorizeSubscription(input)))
|
|
70
|
+
throw new Unauthorized("actor subscription is not authorized");
|
|
71
|
+
this.bind(input);
|
|
72
|
+
const payloadNames = stringList(input.payload.payloads ?? []);
|
|
73
|
+
const definition = this.definition(input.actorType);
|
|
74
|
+
for (const name of payloadNames) {
|
|
75
|
+
if (!definition.payloads[name])
|
|
76
|
+
throw new UnknownPayloadBroadcast(`unknown payload ${name}`);
|
|
77
|
+
}
|
|
78
|
+
if (input.method === "subscribe") {
|
|
79
|
+
const expiresAt = Number(input.payload.expiresAt);
|
|
80
|
+
if (!Number.isFinite(expiresAt) || expiresAt <= Date.now())
|
|
81
|
+
throw new Unauthorized("subscription expired");
|
|
82
|
+
await this.store.atomic(() => this.store.saveSubscription({
|
|
83
|
+
id: String(input.payload.subscriptionId),
|
|
84
|
+
sessionName: String(input.payload.sessionName),
|
|
85
|
+
payloads: payloadNames,
|
|
86
|
+
expiresAt,
|
|
87
|
+
}));
|
|
88
|
+
}
|
|
89
|
+
return this.projection({ input, payloadNames });
|
|
90
|
+
}
|
|
91
|
+
async pump() {
|
|
92
|
+
await this.store.atomic(() => {
|
|
93
|
+
this.store.prune();
|
|
94
|
+
this.scheduleReminders();
|
|
95
|
+
if (this.actorRunning) {
|
|
96
|
+
const head = this.store.head();
|
|
97
|
+
if (head?.status === "claimed") {
|
|
98
|
+
head.availableAt = Date.now() + RECOVERY_INTERVAL;
|
|
99
|
+
this.store.saveMessage(head);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
for (const outbox of this.store.outboxHeads()) {
|
|
103
|
+
if (!this.delivering.has(outbox.id))
|
|
104
|
+
continue;
|
|
105
|
+
outbox.availableAt = Date.now() + RECOVERY_INTERVAL;
|
|
106
|
+
this.store.saveOutbox(outbox);
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
const work = [];
|
|
110
|
+
if (!this.actorRunning)
|
|
111
|
+
work.push(this.drainActors());
|
|
112
|
+
for (const outbox of this.store.outboxHeads()) {
|
|
113
|
+
if (outbox.availableAt > Date.now() || this.delivering.has(outbox.id))
|
|
114
|
+
continue;
|
|
115
|
+
work.push(this.deliver(outbox));
|
|
116
|
+
}
|
|
117
|
+
await Promise.all(work);
|
|
118
|
+
await this.store.atomic(() => undefined);
|
|
119
|
+
}
|
|
120
|
+
bind(identity) {
|
|
121
|
+
const name = actorName(identity);
|
|
122
|
+
const existing = this.store.metadata("identity");
|
|
123
|
+
if (existing !== undefined && existing !== name)
|
|
124
|
+
throw new Unauthorized("Durable Object identity mismatch");
|
|
125
|
+
this.definition(identity.actorType);
|
|
126
|
+
if (existing === undefined)
|
|
127
|
+
this.store.saveMetadata("identity", name);
|
|
128
|
+
}
|
|
129
|
+
definition(type) {
|
|
130
|
+
const definition = this.definitions.get(type);
|
|
131
|
+
if (!definition)
|
|
132
|
+
throw new UnknownActorType(`unknown actor type ${type}`);
|
|
133
|
+
return definition;
|
|
134
|
+
}
|
|
135
|
+
ensureInstance(identity) {
|
|
136
|
+
const existing = this.store.instance();
|
|
137
|
+
if (existing)
|
|
138
|
+
return existing;
|
|
139
|
+
const definition = this.definition(identity.actorType);
|
|
140
|
+
const incarnationOrder = String(BigInt(this.store.metadata("incarnationOrder") ?? "0") + 1n);
|
|
141
|
+
this.store.saveMetadata("incarnationOrder", incarnationOrder);
|
|
142
|
+
const instance = {
|
|
143
|
+
actorType: identity.actorType,
|
|
144
|
+
actorId: identity.actorId,
|
|
145
|
+
incarnation: crypto.randomUUID(),
|
|
146
|
+
incarnationOrder,
|
|
147
|
+
generation: "1",
|
|
148
|
+
revision: "0",
|
|
149
|
+
nextSequence: "1",
|
|
150
|
+
state: initialStateFor(definition),
|
|
151
|
+
stateVersion: definition.stateVersion,
|
|
152
|
+
createdAt: Date.now(),
|
|
153
|
+
paused: false,
|
|
154
|
+
};
|
|
155
|
+
this.store.saveInstance(instance);
|
|
156
|
+
return instance;
|
|
157
|
+
}
|
|
158
|
+
enqueue(input) {
|
|
159
|
+
const definition = this.definition(input.actorType);
|
|
160
|
+
const operation = String(input.payload.operation);
|
|
161
|
+
const deliveryMode = input.method === "internal" ? "internal" : input.payload.deliveryMode;
|
|
162
|
+
if (deliveryMode !== "sync" && deliveryMode !== "async" && deliveryMode !== "internal")
|
|
163
|
+
throw new TypeError("invalid delivery mode");
|
|
164
|
+
if (!definition.operations.includes(operation) &&
|
|
165
|
+
!(deliveryMode === "sync" && definition.queries.includes(operation)))
|
|
166
|
+
throw new UnknownOperation(`unknown operation ${operation}`);
|
|
167
|
+
const argumentsValue = jsonObject(input.payload.arguments, {
|
|
168
|
+
maxBytes: this.settings.maxPayloadBytes,
|
|
169
|
+
});
|
|
170
|
+
const requestId = String(input.payload.requestId);
|
|
171
|
+
if (requestId.length === 0 || requestId.length > 512)
|
|
172
|
+
throw new TypeError("invalid request ID");
|
|
173
|
+
const availableAt = Number(input.payload.availableAt);
|
|
174
|
+
if (!Number.isFinite(availableAt))
|
|
175
|
+
throw new TypeError("invalid availability time");
|
|
176
|
+
const idempotencyKey = input.payload.idempotencyKey === null || input.payload.idempotencyKey === undefined
|
|
177
|
+
? null
|
|
178
|
+
: String(input.payload.idempotencyKey);
|
|
179
|
+
const instance = this.ensureInstance(input);
|
|
180
|
+
const receipt = this.store.storage.sql
|
|
181
|
+
.exec("SELECT message_id FROM receipts WHERE request_id = ?", requestId)
|
|
182
|
+
.toArray()[0];
|
|
183
|
+
const previous = receipt
|
|
184
|
+
? this.store.message(receipt.message_id)
|
|
185
|
+
: idempotencyKey === null
|
|
186
|
+
? undefined
|
|
187
|
+
: this.store.rows("SELECT record FROM messages WHERE incarnation = ? AND idempotency_key = ?", [instance.incarnation, idempotencyKey])[0];
|
|
188
|
+
if (previous) {
|
|
189
|
+
if (previous.incarnation !== instance.incarnation)
|
|
190
|
+
throw new ActorDestroyed("the accepted message belongs to a destroyed actor");
|
|
191
|
+
if (previous.operation !== operation ||
|
|
192
|
+
previous.deliveryMode !== deliveryMode ||
|
|
193
|
+
stableJson(previous.arguments) !== stableJson(argumentsValue))
|
|
194
|
+
throw new IdempotencyConflict("request ID or idempotency key already identifies different work");
|
|
195
|
+
this.store.storage.sql.exec("INSERT OR IGNORE INTO receipts(request_id, message_id) VALUES (?, ?)", requestId, previous.id);
|
|
196
|
+
return previous;
|
|
197
|
+
}
|
|
198
|
+
const count = this.store.storage.sql
|
|
199
|
+
.exec("SELECT COUNT(*) AS count FROM messages WHERE incarnation = ? AND status IN ('ready', 'claimed')", instance.incarnation)
|
|
200
|
+
.one().count;
|
|
201
|
+
if (count >= this.settings.maxMailboxLength)
|
|
202
|
+
throw new MailboxFull("actor mailbox is full");
|
|
203
|
+
if (BigInt(instance.nextSequence) > 9223372036854775807n)
|
|
204
|
+
throw new NonRetryableError("actor mailbox sequence exhausted");
|
|
205
|
+
const message = {
|
|
206
|
+
id: crypto.randomUUID(),
|
|
207
|
+
requestId,
|
|
208
|
+
incarnation: instance.incarnation,
|
|
209
|
+
sequence: instance.nextSequence,
|
|
210
|
+
operation,
|
|
211
|
+
arguments: argumentsValue,
|
|
212
|
+
deliveryMode,
|
|
213
|
+
idempotencyKey,
|
|
214
|
+
status: "ready",
|
|
215
|
+
attempt: 0,
|
|
216
|
+
availableAt,
|
|
217
|
+
createdAt: Date.now(),
|
|
218
|
+
completedAt: null,
|
|
219
|
+
result: null,
|
|
220
|
+
error: null,
|
|
221
|
+
rejection: null,
|
|
222
|
+
generation: null,
|
|
223
|
+
reminder: null,
|
|
224
|
+
};
|
|
225
|
+
instance.nextSequence = String(BigInt(instance.nextSequence) + 1n);
|
|
226
|
+
this.store.saveInstance(instance);
|
|
227
|
+
this.store.saveMessage(message);
|
|
228
|
+
this.store.storage.sql.exec("INSERT INTO receipts(request_id, message_id) VALUES (?, ?)", requestId, message.id);
|
|
229
|
+
return message;
|
|
230
|
+
}
|
|
231
|
+
async authorizeOperation(input, message) {
|
|
232
|
+
const query = this.definitions.get(input.actorType)?.queries.includes(message.operation) ?? false;
|
|
233
|
+
const policy = query ? this.settings.authorizeQuery : this.settings.authorizeMessage;
|
|
234
|
+
if (!(await policy({ ...input, operation: message.operation, arguments: message.arguments })))
|
|
235
|
+
throw new Unauthorized("actor operation is not authorized");
|
|
236
|
+
}
|
|
237
|
+
async readMessage(input) {
|
|
238
|
+
let message;
|
|
239
|
+
if (input.method === "lookup") {
|
|
240
|
+
if (!(await this.settings.authorizeQuery({
|
|
241
|
+
...input,
|
|
242
|
+
operation: "__lookupMessage__",
|
|
243
|
+
arguments: {},
|
|
244
|
+
})))
|
|
245
|
+
throw new Unauthorized("message lookup is not authorized");
|
|
246
|
+
const receipt = this.store.storage.sql
|
|
247
|
+
.exec("SELECT message_id FROM receipts WHERE request_id = ?", String(input.payload.requestId))
|
|
248
|
+
.toArray()[0];
|
|
249
|
+
message = receipt ? this.store.message(receipt.message_id) : undefined;
|
|
250
|
+
if (!message)
|
|
251
|
+
return null;
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
message = this.store.message(String(input.payload.id));
|
|
255
|
+
if (!message ||
|
|
256
|
+
message.requestId !== input.payload.requestId ||
|
|
257
|
+
message.sequence !== input.payload.sequence)
|
|
258
|
+
throw new Unauthorized("message reference is not authorized");
|
|
259
|
+
}
|
|
260
|
+
await this.authorizeOperation(input, message);
|
|
261
|
+
this.bind(input);
|
|
262
|
+
if (message.incarnation !== this.store.instance()?.incarnation)
|
|
263
|
+
throw new ActorDestroyed("actor was destroyed");
|
|
264
|
+
return normalizeJson(message);
|
|
265
|
+
}
|
|
266
|
+
committed(identity) {
|
|
267
|
+
const definition = this.definition(identity.actorType);
|
|
268
|
+
const instance = this.store.instance();
|
|
269
|
+
const state = instance
|
|
270
|
+
? migrateState({
|
|
271
|
+
definition,
|
|
272
|
+
storedVersion: instance.stateVersion,
|
|
273
|
+
storedState: instance.state,
|
|
274
|
+
})
|
|
275
|
+
: initialStateFor(definition);
|
|
276
|
+
return { definition, instance, state };
|
|
277
|
+
}
|
|
278
|
+
async snapshot(identity) {
|
|
279
|
+
const { definition, instance, state } = this.committed(identity);
|
|
280
|
+
const actor = hydrateActor({ definition, actorId: identity.actorId, state });
|
|
281
|
+
const before = stableJson(actorState(actor, definition.stateKeys));
|
|
282
|
+
const snapshot = { ...state };
|
|
283
|
+
await withActorProjection({ actor, runtime: this.runtime }, async () => {
|
|
284
|
+
for (const query of definition.queries) {
|
|
285
|
+
if (definition.stateKeys.includes(query))
|
|
286
|
+
continue;
|
|
287
|
+
const value = await actor.invoke(query, {});
|
|
288
|
+
snapshot[query] = normalizeJson(value === undefined ? null : value, {
|
|
289
|
+
maxBytes: this.settings.maxResultBytes,
|
|
290
|
+
});
|
|
291
|
+
}
|
|
292
|
+
});
|
|
293
|
+
if (stableJson(actorState(actor, definition.stateKeys)) !== before || actor.hasIntents())
|
|
294
|
+
throw new QueryMutatedState("snapshot getters must not mutate state or stage work");
|
|
295
|
+
return {
|
|
296
|
+
snapshot,
|
|
297
|
+
instanceId: instance?.incarnation ?? "0",
|
|
298
|
+
revision: instance?.revision ?? "0",
|
|
299
|
+
createdAtMs: instance?.createdAt ?? 0,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
async projection(options) {
|
|
303
|
+
const { input, payloadNames } = options;
|
|
304
|
+
const { definition, instance, state } = this.committed(input);
|
|
305
|
+
const actor = hydrateActor({ definition, actorId: input.actorId, state });
|
|
306
|
+
const identity = {
|
|
307
|
+
actorType: input.actorType,
|
|
308
|
+
actorId: input.actorId,
|
|
309
|
+
instanceId: instance?.incarnation ?? "0",
|
|
310
|
+
revision: instance?.revision ?? "0",
|
|
311
|
+
};
|
|
312
|
+
const event = {
|
|
313
|
+
version: 1,
|
|
314
|
+
kind: "invalidation",
|
|
315
|
+
...identity,
|
|
316
|
+
...selectActorBroadcast(readActorObservables({ actor, definition, runtime: this.runtime })),
|
|
317
|
+
};
|
|
318
|
+
const payloads = [];
|
|
319
|
+
for (const name of payloadNames) {
|
|
320
|
+
try {
|
|
321
|
+
if (!(await this.settings.authorizeQuery({ ...input, operation: name, arguments: {} })))
|
|
322
|
+
continue;
|
|
323
|
+
const projected = hydrateActor({
|
|
324
|
+
definition,
|
|
325
|
+
actorId: input.actorId,
|
|
326
|
+
state: deepCopy(state),
|
|
327
|
+
});
|
|
328
|
+
const before = stableJson(actorState(projected, definition.stateKeys));
|
|
329
|
+
const handler = definition.payloads[name];
|
|
330
|
+
const value = await withActorProjection({ actor: projected, runtime: this.runtime }, () => handler(projected, input.authorizationContext));
|
|
331
|
+
if (stableJson(actorState(projected, definition.stateKeys)) !== before ||
|
|
332
|
+
projected.hasIntents())
|
|
333
|
+
throw new QueryMutatedState("payload projection mutated state");
|
|
334
|
+
const payload = normalizeJson(value, { maxBytes: this.settings.maxPayloadBytes });
|
|
335
|
+
if (payload === null || typeof payload !== "object")
|
|
336
|
+
throw new TypeError("payload must be an object or array");
|
|
337
|
+
payloads.push({ version: 1, kind: "payload", ...identity, name, payload });
|
|
338
|
+
}
|
|
339
|
+
catch (error) {
|
|
340
|
+
this.emit("payload_broadcast.failed", { payload: name, errorName: errorName(error) });
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
event,
|
|
345
|
+
payloads,
|
|
346
|
+
incarnationOrder: instance?.incarnationOrder ?? this.store.metadata("incarnationOrder") ?? "0",
|
|
347
|
+
};
|
|
348
|
+
}
|
|
349
|
+
destroy() {
|
|
350
|
+
const instance = this.store.instance();
|
|
351
|
+
if (!instance)
|
|
352
|
+
return false;
|
|
353
|
+
this.store.storage.sql.exec("DELETE FROM metadata WHERE key = 'instance'");
|
|
354
|
+
this.store.storage.sql.exec("DELETE FROM outboxes");
|
|
355
|
+
this.store.storage.sql.exec("DELETE FROM reminders");
|
|
356
|
+
for (const message of this.store.rows("SELECT record FROM messages WHERE incarnation = ? AND completed_at IS NULL", [instance.incarnation])) {
|
|
357
|
+
message.status = "completed";
|
|
358
|
+
message.completedAt = Date.now();
|
|
359
|
+
this.store.saveMessage(message);
|
|
360
|
+
}
|
|
361
|
+
this.cached = undefined;
|
|
362
|
+
this.emit("actor.destroyed");
|
|
363
|
+
return true;
|
|
364
|
+
}
|
|
365
|
+
async drainActors() {
|
|
366
|
+
this.actorRunning = true;
|
|
367
|
+
const started = Date.now();
|
|
368
|
+
try {
|
|
369
|
+
for (let count = 0; count < this.settings.maxMessagesPerActivationPass; count += 1) {
|
|
370
|
+
if (Date.now() - started >= this.settings.maxActivationDurationMilliseconds)
|
|
371
|
+
break;
|
|
372
|
+
const head = this.store.head();
|
|
373
|
+
if (!head || head.status !== "ready" || head.availableAt > Date.now())
|
|
374
|
+
break;
|
|
375
|
+
await this.execute(head);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
finally {
|
|
379
|
+
this.actorRunning = false;
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
async execute(message) {
|
|
383
|
+
const instance = this.store.instance();
|
|
384
|
+
const definition = this.definition(instance.actorType);
|
|
385
|
+
let actor;
|
|
386
|
+
try {
|
|
387
|
+
actor =
|
|
388
|
+
this.cached?.incarnation === instance.incarnation
|
|
389
|
+
? this.cached.actor
|
|
390
|
+
: hydrateActor({
|
|
391
|
+
definition,
|
|
392
|
+
actorId: instance.actorId,
|
|
393
|
+
state: migrateState({
|
|
394
|
+
definition,
|
|
395
|
+
storedVersion: instance.stateVersion,
|
|
396
|
+
storedState: instance.state,
|
|
397
|
+
}),
|
|
398
|
+
});
|
|
399
|
+
if (this.cached?.actor !== actor) {
|
|
400
|
+
await withActorContext({ actor, runtime: this.runtime }, () => actor.activate());
|
|
401
|
+
this.assertCurrent(instance);
|
|
402
|
+
this.cached = { incarnation: instance.incarnation, actor };
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch (error) {
|
|
406
|
+
if (error instanceof ActorDestroyed)
|
|
407
|
+
return;
|
|
408
|
+
await this.store.atomic(() => {
|
|
409
|
+
if (this.store.instance()?.incarnation !== instance.incarnation)
|
|
410
|
+
return;
|
|
411
|
+
message.availableAt = Date.now() + RECOVERY_INTERVAL;
|
|
412
|
+
message.error = {
|
|
413
|
+
name: "ActorSetupFailed",
|
|
414
|
+
message: "actor setup failed",
|
|
415
|
+
cause: {
|
|
416
|
+
name: errorName(error),
|
|
417
|
+
message: error instanceof Error ? error.message.slice(0, 4_096) : "actor setup failed",
|
|
418
|
+
},
|
|
419
|
+
};
|
|
420
|
+
this.store.saveMessage(message);
|
|
421
|
+
});
|
|
422
|
+
this.emit("actor.setup_failed", { errorName: errorName(error) });
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
const stateBefore = deepCopy(actorState(actor, definition.stateKeys));
|
|
426
|
+
try {
|
|
427
|
+
await this.store.atomic(() => {
|
|
428
|
+
this.assertCurrent(instance);
|
|
429
|
+
message.status = "claimed";
|
|
430
|
+
message.generation = instance.generation;
|
|
431
|
+
message.attempt += 1;
|
|
432
|
+
message.availableAt = Date.now() + RECOVERY_INTERVAL;
|
|
433
|
+
this.store.saveMessage(message);
|
|
434
|
+
});
|
|
435
|
+
this.emit("message.started", { messageId: message.id, attempt: message.attempt });
|
|
436
|
+
const evaluated = await evaluateActorTurn({
|
|
437
|
+
actor,
|
|
438
|
+
definition,
|
|
439
|
+
runtime: this.runtime,
|
|
440
|
+
stateBefore,
|
|
441
|
+
operation: message.operation,
|
|
442
|
+
argumentsValue: message.arguments,
|
|
443
|
+
message: {
|
|
444
|
+
id: message.id,
|
|
445
|
+
requestId: message.requestId,
|
|
446
|
+
actorType: instance.actorType,
|
|
447
|
+
actorId: instance.actorId,
|
|
448
|
+
sequence: BigInt(message.sequence),
|
|
449
|
+
attempt: message.attempt,
|
|
450
|
+
enqueuedAt: new Date(message.createdAt),
|
|
451
|
+
idempotencyKey: message.idempotencyKey,
|
|
452
|
+
},
|
|
453
|
+
maxStateBytes: this.settings.maxStateBytes,
|
|
454
|
+
maxResultBytes: this.settings.maxResultBytes,
|
|
455
|
+
});
|
|
456
|
+
const intents = actor.drainIntents();
|
|
457
|
+
if (intents.commitActions.length > 0)
|
|
458
|
+
throw new UnsupportedCapability("the Durable Objects backend does not support commitAction");
|
|
459
|
+
await this.store.atomic(() => {
|
|
460
|
+
const current = this.assertCurrent(instance);
|
|
461
|
+
current.state = evaluated.state;
|
|
462
|
+
current.stateVersion = definition.stateVersion;
|
|
463
|
+
current.revision = message.sequence;
|
|
464
|
+
this.store.saveInstance(current);
|
|
465
|
+
message.status = "completed";
|
|
466
|
+
message.result = evaluated.result;
|
|
467
|
+
message.completedAt = Date.now();
|
|
468
|
+
this.store.saveMessage(message);
|
|
469
|
+
this.stage({ instance: current, message, intents, broadcast: evaluated.broadcast });
|
|
470
|
+
this.completeReminder(message);
|
|
471
|
+
});
|
|
472
|
+
this.emit("message.completed", { messageId: message.id });
|
|
473
|
+
}
|
|
474
|
+
catch (error) {
|
|
475
|
+
actor.discardIntents();
|
|
476
|
+
for (const key of definition.stateKeys)
|
|
477
|
+
Object.assign(actor, { [key]: deepCopy(stateBefore[key]) });
|
|
478
|
+
if (error instanceof ActorDestroyed)
|
|
479
|
+
return;
|
|
480
|
+
await this.store.atomic(() => {
|
|
481
|
+
const current = this.store.instance();
|
|
482
|
+
if (!current ||
|
|
483
|
+
current.incarnation !== instance.incarnation ||
|
|
484
|
+
current.generation !== instance.generation)
|
|
485
|
+
return;
|
|
486
|
+
message.result = null;
|
|
487
|
+
message.completedAt = null;
|
|
488
|
+
message.rejection = null;
|
|
489
|
+
if (error instanceof Rejected) {
|
|
490
|
+
message.status = "rejected";
|
|
491
|
+
message.rejection = {
|
|
492
|
+
code: error.code,
|
|
493
|
+
message: error.message,
|
|
494
|
+
details: jsonObject(error.details),
|
|
495
|
+
};
|
|
496
|
+
message.completedAt = Date.now();
|
|
497
|
+
this.completeReminder(message);
|
|
498
|
+
}
|
|
499
|
+
else {
|
|
500
|
+
message.error = {
|
|
501
|
+
name: errorName(error),
|
|
502
|
+
message: error instanceof Error ? error.message.slice(0, 4_096) : "operation failed",
|
|
503
|
+
};
|
|
504
|
+
const exhausted = error instanceof NonRetryableError || message.attempt >= this.settings.maxAttempts;
|
|
505
|
+
message.status = exhausted ? "dead" : "ready";
|
|
506
|
+
message.availableAt = Date.now() + this.retryDelay(message.attempt);
|
|
507
|
+
if (exhausted) {
|
|
508
|
+
current.paused = true;
|
|
509
|
+
this.store.saveInstance(current);
|
|
510
|
+
this.pauseReminder(message);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
try {
|
|
514
|
+
this.store.saveMessage(message);
|
|
515
|
+
}
|
|
516
|
+
catch (storageError) {
|
|
517
|
+
if (!(storageError instanceof PayloadTooLarge))
|
|
518
|
+
throw storageError;
|
|
519
|
+
message.status = "dead";
|
|
520
|
+
message.completedAt = null;
|
|
521
|
+
message.rejection = null;
|
|
522
|
+
message.error = { name: storageError.name, message: storageError.message };
|
|
523
|
+
current.paused = true;
|
|
524
|
+
this.store.saveInstance(current);
|
|
525
|
+
this.pauseReminder(message);
|
|
526
|
+
this.store.saveMessage(message);
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
this.emit("message.failed", {
|
|
530
|
+
messageId: message.id,
|
|
531
|
+
errorName: errorName(error),
|
|
532
|
+
status: message.status,
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
assertCurrent(instance) {
|
|
537
|
+
const current = this.store.instance();
|
|
538
|
+
if (!current ||
|
|
539
|
+
current.incarnation !== instance.incarnation ||
|
|
540
|
+
current.generation !== instance.generation)
|
|
541
|
+
throw new ActorDestroyed("actor incarnation or execution generation changed");
|
|
542
|
+
return current;
|
|
543
|
+
}
|
|
544
|
+
stage(options) {
|
|
545
|
+
const { instance, message, intents, broadcast } = options;
|
|
546
|
+
for (const effect of intents.effects) {
|
|
547
|
+
const id = crypto.randomUUID();
|
|
548
|
+
this.addOutbox({
|
|
549
|
+
id,
|
|
550
|
+
instance,
|
|
551
|
+
message,
|
|
552
|
+
kind: "effect",
|
|
553
|
+
destination: id,
|
|
554
|
+
payload: jsonObject(effect),
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
for (const outbound of intents.outboundMessages) {
|
|
558
|
+
this.addOutbox({
|
|
559
|
+
id: crypto.randomUUID(),
|
|
560
|
+
instance,
|
|
561
|
+
message,
|
|
562
|
+
kind: "outbound",
|
|
563
|
+
destination: actorName(outbound),
|
|
564
|
+
payload: jsonObject(outbound),
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
for (const intent of intents.reminders) {
|
|
568
|
+
this.store.saveReminder({
|
|
569
|
+
name: intent.name,
|
|
570
|
+
generation: crypto.randomUUID(),
|
|
571
|
+
operation: intent.operation,
|
|
572
|
+
arguments: intent.arguments,
|
|
573
|
+
at: intent.atMilliseconds,
|
|
574
|
+
interval: intent.intervalMilliseconds ?? null,
|
|
575
|
+
missed: intent.missedPolicy,
|
|
576
|
+
status: "scheduled",
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
if (!broadcast)
|
|
580
|
+
return;
|
|
581
|
+
for (const subscription of this.store.rows("SELECT record FROM subscriptions WHERE expires_at > ?", [Date.now()])) {
|
|
582
|
+
this.addOutbox({
|
|
583
|
+
id: crypto.randomUUID(),
|
|
584
|
+
instance,
|
|
585
|
+
message,
|
|
586
|
+
kind: "broadcast",
|
|
587
|
+
destination: subscription.id,
|
|
588
|
+
payload: {
|
|
589
|
+
sessionName: subscription.sessionName,
|
|
590
|
+
event: {
|
|
591
|
+
version: 1,
|
|
592
|
+
kind: "invalidation",
|
|
593
|
+
actorType: instance.actorType,
|
|
594
|
+
actorId: instance.actorId,
|
|
595
|
+
instanceId: instance.incarnation,
|
|
596
|
+
revision: message.sequence,
|
|
597
|
+
...broadcast,
|
|
598
|
+
},
|
|
599
|
+
},
|
|
600
|
+
});
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
addOutbox(options) {
|
|
604
|
+
this.store.saveOutbox({
|
|
605
|
+
id: options.id,
|
|
606
|
+
incarnation: options.instance.incarnation,
|
|
607
|
+
messageId: options.message.id,
|
|
608
|
+
sequence: options.message.sequence,
|
|
609
|
+
kind: options.kind,
|
|
610
|
+
destination: options.destination,
|
|
611
|
+
payload: options.payload,
|
|
612
|
+
status: "pending",
|
|
613
|
+
attempt: 0,
|
|
614
|
+
availableAt: Date.now(),
|
|
615
|
+
completedAt: null,
|
|
616
|
+
error: null,
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
async deliver(outbox) {
|
|
620
|
+
this.delivering.add(outbox.id);
|
|
621
|
+
const instance = this.store.instance();
|
|
622
|
+
if (!instance || instance.incarnation !== outbox.incarnation) {
|
|
623
|
+
this.delivering.delete(outbox.id);
|
|
624
|
+
return;
|
|
625
|
+
}
|
|
626
|
+
try {
|
|
627
|
+
await this.store.atomic(() => {
|
|
628
|
+
if (!this.outboxCurrent(outbox, instance))
|
|
629
|
+
throw new ActorDestroyed("outbox was removed");
|
|
630
|
+
outbox.status = "claimed";
|
|
631
|
+
outbox.attempt += 1;
|
|
632
|
+
outbox.availableAt = Date.now() + RECOVERY_INTERVAL;
|
|
633
|
+
this.store.saveOutbox(outbox);
|
|
634
|
+
});
|
|
635
|
+
let result = null;
|
|
636
|
+
if (outbox.kind === "effect") {
|
|
637
|
+
const handler = this.settings.effects[String(outbox.payload.name)];
|
|
638
|
+
if (!handler)
|
|
639
|
+
throw new UnknownEffect(`unknown effect ${String(outbox.payload.name)}`);
|
|
640
|
+
const value = await withRuntime(this.runtime, () => handler(jsonObject(outbox.payload.arguments), {
|
|
641
|
+
id: outbox.id,
|
|
642
|
+
attempt: outbox.attempt,
|
|
643
|
+
sourceMessageId: outbox.messageId,
|
|
644
|
+
actorType: instance.actorType,
|
|
645
|
+
actorId: instance.actorId,
|
|
646
|
+
}));
|
|
647
|
+
result = normalizeJson(value === undefined ? null : value, {
|
|
648
|
+
maxBytes: this.settings.maxResultBytes,
|
|
649
|
+
});
|
|
650
|
+
}
|
|
651
|
+
else if (outbox.kind === "effect-callback") {
|
|
652
|
+
await callHost({
|
|
653
|
+
backend: this.settings.backend,
|
|
654
|
+
request: {
|
|
655
|
+
actorType: instance.actorType,
|
|
656
|
+
actorId: instance.actorId,
|
|
657
|
+
method: "internal",
|
|
658
|
+
authorizationContext: null,
|
|
659
|
+
payload: {
|
|
660
|
+
requestId: outbox.id,
|
|
661
|
+
operation: String(outbox.payload.operation),
|
|
662
|
+
arguments: jsonObject(outbox.payload.arguments),
|
|
663
|
+
availableAt: Date.now(),
|
|
664
|
+
idempotencyKey: outbox.id,
|
|
665
|
+
},
|
|
666
|
+
},
|
|
667
|
+
});
|
|
668
|
+
}
|
|
669
|
+
else if (outbox.kind === "outbound") {
|
|
670
|
+
await callHost({
|
|
671
|
+
backend: this.settings.backend,
|
|
672
|
+
request: {
|
|
673
|
+
actorType: String(outbox.payload.actorType),
|
|
674
|
+
actorId: String(outbox.payload.actorId),
|
|
675
|
+
method: "internal",
|
|
676
|
+
authorizationContext: null,
|
|
677
|
+
payload: {
|
|
678
|
+
requestId: outbox.id,
|
|
679
|
+
operation: outbox.payload.operation,
|
|
680
|
+
arguments: outbox.payload.arguments,
|
|
681
|
+
availableAt: outbox.payload.availableAtMilliseconds ?? Date.now(),
|
|
682
|
+
idempotencyKey: outbox.payload.idempotencyKey ?? outbox.id,
|
|
683
|
+
},
|
|
684
|
+
},
|
|
685
|
+
});
|
|
686
|
+
}
|
|
687
|
+
else {
|
|
688
|
+
const subscription = this.store.rows("SELECT record FROM subscriptions WHERE id = ? AND expires_at > ?", [outbox.destination, Date.now()])[0];
|
|
689
|
+
if (subscription && this.settings.backend.sessions) {
|
|
690
|
+
await beforeDeadline(this.settings.backend.sessions.getByName(subscription.sessionName).publish({
|
|
691
|
+
subscriptionId: subscription.id,
|
|
692
|
+
event: jsonObject(outbox.payload.event),
|
|
693
|
+
}), 5_000);
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
await this.store.atomic(() => {
|
|
697
|
+
if (!this.outboxCurrent(outbox, instance))
|
|
698
|
+
return;
|
|
699
|
+
outbox.status = "completed";
|
|
700
|
+
outbox.completedAt = Date.now();
|
|
701
|
+
this.store.saveOutbox(outbox);
|
|
702
|
+
if (outbox.kind === "effect")
|
|
703
|
+
this.stageEffectCallback({
|
|
704
|
+
instance,
|
|
705
|
+
outbox,
|
|
706
|
+
result,
|
|
707
|
+
operation: outbox.payload.successOperation,
|
|
708
|
+
});
|
|
709
|
+
});
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
await this.store.atomic(() => {
|
|
713
|
+
if (!this.outboxCurrent(outbox, instance))
|
|
714
|
+
return;
|
|
715
|
+
const exhausted = error instanceof NonRetryableError || outbox.attempt >= this.settings.maxAttempts;
|
|
716
|
+
outbox.status = exhausted ? "dead" : "pending";
|
|
717
|
+
outbox.error = {
|
|
718
|
+
name: errorName(error),
|
|
719
|
+
message: error instanceof Error ? error.message : "delivery failed",
|
|
720
|
+
};
|
|
721
|
+
outbox.availableAt = Date.now() + this.retryDelay(outbox.attempt);
|
|
722
|
+
this.store.saveOutbox(outbox);
|
|
723
|
+
if (exhausted && outbox.kind === "effect")
|
|
724
|
+
this.stageEffectCallback({
|
|
725
|
+
instance,
|
|
726
|
+
outbox,
|
|
727
|
+
result: outbox.error,
|
|
728
|
+
operation: outbox.payload.failureOperation,
|
|
729
|
+
});
|
|
730
|
+
});
|
|
731
|
+
this.emit("outbox.failed", {
|
|
732
|
+
outboxId: outbox.id,
|
|
733
|
+
kind: outbox.kind,
|
|
734
|
+
errorName: errorName(error),
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
finally {
|
|
738
|
+
this.delivering.delete(outbox.id);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
outboxCurrent(outbox, instance) {
|
|
742
|
+
const current = this.store.instance();
|
|
743
|
+
return (current?.incarnation === outbox.incarnation &&
|
|
744
|
+
current.generation === instance.generation &&
|
|
745
|
+
this.store.rows("SELECT record FROM outboxes WHERE id = ?", [outbox.id]).length > 0);
|
|
746
|
+
}
|
|
747
|
+
stageEffectCallback(options) {
|
|
748
|
+
if (typeof options.operation !== "string")
|
|
749
|
+
return;
|
|
750
|
+
this.addOutbox({
|
|
751
|
+
id: `${options.outbox.id}:callback`,
|
|
752
|
+
instance: options.instance,
|
|
753
|
+
message: {
|
|
754
|
+
id: options.outbox.messageId,
|
|
755
|
+
sequence: options.outbox.sequence,
|
|
756
|
+
},
|
|
757
|
+
kind: "effect-callback",
|
|
758
|
+
destination: actorName(options.instance),
|
|
759
|
+
payload: {
|
|
760
|
+
operation: options.operation,
|
|
761
|
+
arguments: {
|
|
762
|
+
effectId: options.outbox.id,
|
|
763
|
+
arguments: options.outbox.payload.arguments,
|
|
764
|
+
...(options.outbox.status === "dead"
|
|
765
|
+
? { error: options.result }
|
|
766
|
+
: { result: options.result }),
|
|
767
|
+
},
|
|
768
|
+
},
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
scheduleReminders() {
|
|
772
|
+
const instance = this.store.instance();
|
|
773
|
+
if (!instance || instance.paused)
|
|
774
|
+
return;
|
|
775
|
+
for (const reminder of this.store.rows("SELECT record FROM reminders WHERE status = 'scheduled' AND due_at <= ? ORDER BY due_at LIMIT ?", [Date.now(), this.settings.maxMessagesPerActivationPass])) {
|
|
776
|
+
try {
|
|
777
|
+
const message = this.enqueue({
|
|
778
|
+
...instance,
|
|
779
|
+
method: "internal",
|
|
780
|
+
authorizationContext: null,
|
|
781
|
+
payload: {
|
|
782
|
+
requestId: `reminder:${reminder.generation}:${reminder.at}`,
|
|
783
|
+
operation: reminder.operation,
|
|
784
|
+
arguments: reminder.arguments,
|
|
785
|
+
idempotencyKey: `reminder:${reminder.generation}:${reminder.at}`,
|
|
786
|
+
availableAt: reminder.at,
|
|
787
|
+
},
|
|
788
|
+
});
|
|
789
|
+
message.reminder = { name: reminder.name, generation: reminder.generation };
|
|
790
|
+
this.store.saveMessage(message);
|
|
791
|
+
reminder.status = "completed";
|
|
792
|
+
this.store.saveReminder(reminder);
|
|
793
|
+
}
|
|
794
|
+
catch (error) {
|
|
795
|
+
if (!(error instanceof MailboxFull))
|
|
796
|
+
throw error;
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
completeReminder(message) {
|
|
802
|
+
if (!message.reminder)
|
|
803
|
+
return;
|
|
804
|
+
const reminder = this.store.rows("SELECT record FROM reminders WHERE name = ?", [
|
|
805
|
+
message.reminder.name,
|
|
806
|
+
])[0];
|
|
807
|
+
if (!reminder ||
|
|
808
|
+
reminder.generation !== message.reminder.generation ||
|
|
809
|
+
reminder.interval === null)
|
|
810
|
+
return;
|
|
811
|
+
const steps = reminder.missed === "latest"
|
|
812
|
+
? Math.max(1, Math.floor((Date.now() - reminder.at) / reminder.interval) + 1)
|
|
813
|
+
: 1;
|
|
814
|
+
reminder.at += steps * reminder.interval;
|
|
815
|
+
reminder.status = "scheduled";
|
|
816
|
+
this.store.saveReminder(reminder);
|
|
817
|
+
}
|
|
818
|
+
pauseReminder(message) {
|
|
819
|
+
if (!message.reminder)
|
|
820
|
+
return;
|
|
821
|
+
const reminder = this.store.rows("SELECT record FROM reminders WHERE name = ?", [
|
|
822
|
+
message.reminder.name,
|
|
823
|
+
])[0];
|
|
824
|
+
if (!reminder || reminder.generation !== message.reminder.generation)
|
|
825
|
+
return;
|
|
826
|
+
reminder.status = "paused";
|
|
827
|
+
this.store.saveReminder(reminder);
|
|
828
|
+
}
|
|
829
|
+
async administer(input) {
|
|
830
|
+
const action = String(input.payload.action);
|
|
831
|
+
if (!(await this.settings.authorizeAdministration({
|
|
832
|
+
action,
|
|
833
|
+
resource: `actor:${actorName(input)}`,
|
|
834
|
+
authorizationContext: input.authorizationContext,
|
|
835
|
+
})))
|
|
836
|
+
throw new Unauthorized("actor administration is not authorized");
|
|
837
|
+
this.bind(input);
|
|
838
|
+
if (action === "deadLetters")
|
|
839
|
+
return normalizeJson({
|
|
840
|
+
messages: this.store.rows("SELECT record FROM messages WHERE status = 'dead' ORDER BY sequence LIMIT 1000"),
|
|
841
|
+
outboxes: this.store.rows("SELECT record FROM outboxes WHERE status = 'dead' ORDER BY sequence LIMIT 1000"),
|
|
842
|
+
});
|
|
843
|
+
if (action === "reminders")
|
|
844
|
+
return normalizeJson(this.store.rows("SELECT record FROM reminders ORDER BY due_at LIMIT 1000"));
|
|
845
|
+
return this.store.atomic(() => {
|
|
846
|
+
if (action === "retryDeadLetter") {
|
|
847
|
+
const id = String(input.payload.id);
|
|
848
|
+
const message = this.store.message(id);
|
|
849
|
+
if (message?.status === "dead" &&
|
|
850
|
+
message.incarnation === this.store.instance()?.incarnation) {
|
|
851
|
+
message.status = "ready";
|
|
852
|
+
message.attempt = 0;
|
|
853
|
+
message.availableAt = Date.now();
|
|
854
|
+
this.store.saveMessage(message);
|
|
855
|
+
const instance = this.store.instance();
|
|
856
|
+
instance.paused = false;
|
|
857
|
+
this.store.saveInstance(instance);
|
|
858
|
+
return normalizeJson(message);
|
|
859
|
+
}
|
|
860
|
+
const outbox = this.store.rows("SELECT record FROM outboxes WHERE id = ? AND status = 'dead'", [id])[0];
|
|
861
|
+
if (!outbox)
|
|
862
|
+
throw new UnknownDeadLetter("unknown dead letter");
|
|
863
|
+
outbox.status = "pending";
|
|
864
|
+
outbox.attempt = 0;
|
|
865
|
+
outbox.availableAt = Date.now();
|
|
866
|
+
this.store.saveOutbox(outbox);
|
|
867
|
+
return normalizeJson(outbox);
|
|
868
|
+
}
|
|
869
|
+
if (action === "resumeReminder") {
|
|
870
|
+
const reminder = this.store.rows("SELECT record FROM reminders WHERE name = ?", [
|
|
871
|
+
String(input.payload.name),
|
|
872
|
+
])[0];
|
|
873
|
+
if (!reminder)
|
|
874
|
+
throw new UnknownReminder("unknown reminder");
|
|
875
|
+
if (reminder.status !== "paused")
|
|
876
|
+
throw new ReminderNotPaused("reminder is not paused");
|
|
877
|
+
const at = Number(input.payload.runAt);
|
|
878
|
+
if (!Number.isFinite(at))
|
|
879
|
+
throw new TypeError("invalid reminder runAt");
|
|
880
|
+
reminder.at = at;
|
|
881
|
+
reminder.status = "scheduled";
|
|
882
|
+
reminder.generation = crypto.randomUUID();
|
|
883
|
+
this.store.saveReminder(reminder);
|
|
884
|
+
const instance = this.store.instance();
|
|
885
|
+
if (instance) {
|
|
886
|
+
instance.paused = false;
|
|
887
|
+
this.store.saveInstance(instance);
|
|
888
|
+
}
|
|
889
|
+
return normalizeJson(reminder);
|
|
890
|
+
}
|
|
891
|
+
throw new UnsupportedCapability(`unsupported actor administration action ${action}`);
|
|
892
|
+
});
|
|
893
|
+
}
|
|
894
|
+
retryDelay(attempt) {
|
|
895
|
+
const delay = this.settings.retryDelayMilliseconds(attempt);
|
|
896
|
+
return Number.isFinite(delay) && delay >= 1 ? delay : 1_000;
|
|
897
|
+
}
|
|
898
|
+
emit(name, attributes = {}) {
|
|
899
|
+
try {
|
|
900
|
+
this.settings.instrumentation?.({
|
|
901
|
+
name: `solid_objects.${name}`,
|
|
902
|
+
occurredAt: new Date().toISOString(),
|
|
903
|
+
attributes,
|
|
904
|
+
});
|
|
905
|
+
}
|
|
906
|
+
catch {
|
|
907
|
+
this.settings.logger.error({ event: "solid_objects.instrumentation.failed", name });
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
function errorName(error) {
|
|
912
|
+
return error instanceof Error ? error.name : "Error";
|
|
913
|
+
}
|
|
914
|
+
function stringList(value) {
|
|
915
|
+
if (!Array.isArray(value) ||
|
|
916
|
+
value.length > 50 ||
|
|
917
|
+
!value.every((item) => typeof item === "string"))
|
|
918
|
+
throw new TypeError("payloads must contain at most 50 names");
|
|
919
|
+
return [...new Set(value)];
|
|
920
|
+
}
|
|
921
|
+
//# sourceMappingURL=engine.js.map
|