@effect-agent/storage-cloudflare 0.1.0-beta.49 → 0.1.0-beta.51
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/dist/DoScheduleStore.mjs +1 -1
- package/dist/DoScheduleStore.mjs.map +1 -1
- package/dist/{DoStorageVersion-KrYUXqeJ.d.mts → DoStorageVersion-DxqcVgDE.d.mts} +2 -2
- package/dist/DoStorageVersion.d.mts +1 -1
- package/dist/DoStorageVersion.mjs +2 -2
- package/dist/DoSubmissionLedger.mjs +35 -6
- package/dist/DoSubmissionLedger.mjs.map +1 -1
- package/dist/DoSubscriptionStore.mjs +14 -570
- package/dist/DoSubscriptionStore.mjs.map +1 -1
- package/dist/{DoThreadStore-D00TdRAa.mjs → DoThreadStore-JqjeFOga.mjs} +8 -8
- package/dist/{DoThreadStore-D00TdRAa.mjs.map → DoThreadStore-JqjeFOga.mjs.map} +1 -1
- package/dist/DoThreadStore.mjs +1 -1
- package/dist/PortProtocol.d.mts +33 -7
- package/dist/PortProtocol.mjs +2 -1
- package/dist/PortProtocol.mjs.map +1 -1
- package/dist/PortRouting.mjs +2 -2
- package/dist/PortRouting.mjs.map +1 -1
- package/dist/index.d.mts +1 -1
- package/dist/index.mjs +1 -1
- package/dist/{migrations-Bo4GU-pp.mjs → migrations-B-jiT7vx.mjs} +9 -3
- package/dist/migrations-B-jiT7vx.mjs.map +1 -0
- package/package.json +1 -1
- package/src/DoScheduleStore.ts +1 -1
- package/src/DoSubmissionLedger.ts +61 -3
- package/src/DoSubscriptionStore.ts +21 -1009
- package/src/PortProtocol.ts +2 -0
- package/src/PortRouting.ts +2 -1
- package/src/internal/migrations.ts +8 -1
- package/dist/migrations-Bo4GU-pp.mjs.map +0 -1
|
@@ -1,36 +1,19 @@
|
|
|
1
|
-
import { Digest } from "@effect-agent/thread/Records";
|
|
2
|
-
import { type SubscriptionFailpointError } from "@effect-agent/thread/Subscription";
|
|
3
1
|
import {
|
|
4
|
-
|
|
5
|
-
|
|
2
|
+
makeSqlSubscriptionStore,
|
|
3
|
+
SqlSubscriptionTransaction,
|
|
4
|
+
} from "@effect-agent/thread/SqlSubscriptionStore";
|
|
5
|
+
import {
|
|
6
6
|
SourcePartition,
|
|
7
|
-
SubscriptionDelivery,
|
|
8
|
-
SubscriptionDeliveryKey,
|
|
9
7
|
SubscriptionError,
|
|
10
8
|
SubscriptionFailpoint,
|
|
11
|
-
|
|
12
|
-
SubscriptionLimits,
|
|
13
|
-
SubscriptionName,
|
|
14
|
-
SubscriptionRecord,
|
|
15
|
-
SubscriptionScanCursors,
|
|
9
|
+
type SubscriptionFailpointError,
|
|
16
10
|
SubscriptionStore,
|
|
17
|
-
subscriptionDeliveryKeyString,
|
|
18
11
|
} from "@effect-agent/thread/Subscription";
|
|
19
|
-
import {
|
|
20
|
-
applySubscriptionDeliveryChange,
|
|
21
|
-
sameAcceptedEventIdentity,
|
|
22
|
-
sameSourcePartition,
|
|
23
|
-
subscriptionCanSelect,
|
|
24
|
-
subscriptionDeliveryCanSelect,
|
|
25
|
-
} from "@effect-agent/thread/SubscriptionTransition";
|
|
26
|
-
import { Clock, Context, Effect, Layer, Result, Schema } from "effect";
|
|
12
|
+
import { Context, Effect, Layer, Schema } from "effect";
|
|
27
13
|
import * as SqlClientService from "effect/unstable/sql/SqlClient";
|
|
28
14
|
import type { SqlError } from "effect/unstable/sql/SqlError";
|
|
29
15
|
|
|
30
|
-
const CURRENT_SUBSCRIPTION_STORE_VERSION =
|
|
31
|
-
const StoredJson = Schema.String.check(Schema.isMaxLength(1_900_000));
|
|
32
|
-
const CountRow = Schema.Struct({ count: Schema.Int.check(Schema.isGreaterThanOrEqualTo(0)) });
|
|
33
|
-
const SequenceRow = Schema.Struct({ sequence: Schema.Natural });
|
|
16
|
+
const CURRENT_SUBSCRIPTION_STORE_VERSION = 3;
|
|
34
17
|
|
|
35
18
|
const ScanRow = Schema.Struct({
|
|
36
19
|
event_scan_cursor: Schema.String,
|
|
@@ -38,44 +21,6 @@ const ScanRow = Schema.Struct({
|
|
|
38
21
|
recovery_scan_cursor: Schema.Natural,
|
|
39
22
|
});
|
|
40
23
|
|
|
41
|
-
const JsonRow = Schema.Struct({ record_json: StoredJson });
|
|
42
|
-
|
|
43
|
-
const RegistrationRow = Schema.Struct({
|
|
44
|
-
owner_id: Schema.String,
|
|
45
|
-
subscription_id: Schema.String,
|
|
46
|
-
ordinal: Schema.Natural,
|
|
47
|
-
source_name: Schema.String,
|
|
48
|
-
source_version: Schema.String,
|
|
49
|
-
matching_key: Schema.String,
|
|
50
|
-
state: SubscriptionRecord.fields.state,
|
|
51
|
-
expires_at_millis: Schema.Number,
|
|
52
|
-
recovery_at_millis: Schema.NullOr(Schema.Number),
|
|
53
|
-
record_json: StoredJson,
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
const EventRow = Schema.Struct({
|
|
57
|
-
event_id: Schema.String,
|
|
58
|
-
source_name: Schema.String,
|
|
59
|
-
source_version: Schema.String,
|
|
60
|
-
matching_key: Schema.String,
|
|
61
|
-
payload_digest: Digest,
|
|
62
|
-
cutoff: Schema.Natural,
|
|
63
|
-
cursor: Schema.Natural,
|
|
64
|
-
routing_complete: Schema.Number,
|
|
65
|
-
next_attempt_at_millis: Schema.Number,
|
|
66
|
-
record_json: StoredJson,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
const DeliveryRow = Schema.Struct({
|
|
70
|
-
owner_id: Schema.String,
|
|
71
|
-
subscription_id: Schema.String,
|
|
72
|
-
event_id: Schema.String,
|
|
73
|
-
delivery_key: Schema.String,
|
|
74
|
-
state: SubscriptionDelivery.fields.state,
|
|
75
|
-
next_attempt_at_millis: Schema.Number,
|
|
76
|
-
record_json: StoredJson,
|
|
77
|
-
});
|
|
78
|
-
|
|
79
24
|
const StoreStateRow = Schema.Struct({
|
|
80
25
|
storage_version: Schema.Int,
|
|
81
26
|
alarm_generation: Schema.Natural,
|
|
@@ -115,34 +60,13 @@ const error = (reason: SubscriptionError["reason"], code: string) =>
|
|
|
115
60
|
|
|
116
61
|
const unavailable = (operation: string) => error("storage", operation);
|
|
117
62
|
const corrupt = (operation: string) => error("corrupt", operation);
|
|
118
|
-
const bytes = (value: unknown) => new TextEncoder().encode(JSON.stringify(value)).byteLength;
|
|
119
63
|
|
|
120
64
|
const validate = <A, I>(schema: Schema.Codec<A, I>, value: unknown, code: string) =>
|
|
121
65
|
Schema.decodeUnknownEffect(schema)(value).pipe(Effect.mapError(() => error("validation", code)));
|
|
122
66
|
|
|
123
|
-
const encode = <A, I>(schema: Schema.Codec<A, I>, value: A, code: string) =>
|
|
124
|
-
Schema.encodeEffect(Schema.fromJsonString(schema))(value).pipe(
|
|
125
|
-
Effect.mapError(() => corrupt(code)),
|
|
126
|
-
);
|
|
127
|
-
|
|
128
|
-
const decode = <A, I>(schema: Schema.Codec<A, I>, value: string, code: string) =>
|
|
129
|
-
Schema.decodeEffect(Schema.fromJsonString(schema))(value).pipe(
|
|
130
|
-
Effect.mapError(() => corrupt(code)),
|
|
131
|
-
);
|
|
132
|
-
|
|
133
67
|
const decodeRows = <A, I>(schema: Schema.Codec<A, I>, rows: unknown, code: string) =>
|
|
134
68
|
Schema.decodeUnknownEffect(Schema.Array(schema))(rows).pipe(Effect.mapError(() => corrupt(code)));
|
|
135
69
|
|
|
136
|
-
const sameDeliveryIdentity = (left: SubscriptionDelivery, right: SubscriptionDelivery): boolean =>
|
|
137
|
-
subscriptionDeliveryKeyString(left.key) === subscriptionDeliveryKeyString(right.key) &&
|
|
138
|
-
left.deliveryId === right.deliveryId &&
|
|
139
|
-
left.source.name === right.source.name &&
|
|
140
|
-
left.source.version === right.source.version &&
|
|
141
|
-
left.threadId === right.threadId &&
|
|
142
|
-
left.admissionKey === right.admissionKey &&
|
|
143
|
-
left.subscriptionFingerprint === right.subscriptionFingerprint &&
|
|
144
|
-
left.eventDigest === right.eventDigest;
|
|
145
|
-
|
|
146
70
|
const initializeDoSubscriptionStore = Effect.fn("DoSubscriptionStore.initialize")(function* () {
|
|
147
71
|
const sql = yield* SqlClientService.SqlClient;
|
|
148
72
|
|
|
@@ -184,7 +108,7 @@ const initializeDoSubscriptionStore = Effect.fn("DoSubscriptionStore.initialize"
|
|
|
184
108
|
yield* sql`CREATE TABLE effect_agent_subscriptions (
|
|
185
109
|
tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, owner_id TEXT NOT NULL, subscription_id TEXT NOT NULL,
|
|
186
110
|
ordinal INTEGER NOT NULL, source_name TEXT NOT NULL, source_version TEXT NOT NULL, matching_key TEXT NOT NULL,
|
|
187
|
-
state TEXT NOT NULL, expires_at_millis INTEGER NOT NULL
|
|
111
|
+
state TEXT NOT NULL, expires_at_millis INTEGER, recovery_at_millis INTEGER, recovery_present INTEGER NOT NULL DEFAULT 0, record_json TEXT NOT NULL,
|
|
188
112
|
PRIMARY KEY (tenant_id, source_address, owner_id, subscription_id), UNIQUE (tenant_id, source_address, ordinal)
|
|
189
113
|
)`.withoutTransform;
|
|
190
114
|
yield* sql`CREATE INDEX effect_agent_subscriptions_owner ON effect_agent_subscriptions (tenant_id, source_address, owner_id, ordinal)`
|
|
@@ -196,7 +120,7 @@ const initializeDoSubscriptionStore = Effect.fn("DoSubscriptionStore.initialize"
|
|
|
196
120
|
yield* sql`CREATE TABLE effect_agent_subscription_events (
|
|
197
121
|
tenant_id TEXT NOT NULL, source_address TEXT NOT NULL, event_id TEXT NOT NULL, source_name TEXT NOT NULL,
|
|
198
122
|
source_version TEXT NOT NULL, matching_key TEXT NOT NULL, payload_digest TEXT NOT NULL, cutoff INTEGER NOT NULL,
|
|
199
|
-
cursor INTEGER NOT NULL, routing_complete INTEGER NOT NULL, next_attempt_at_millis INTEGER NOT NULL, record_json TEXT NOT NULL,
|
|
123
|
+
cursor INTEGER NOT NULL, routing_complete INTEGER NOT NULL, tombstone INTEGER NOT NULL DEFAULT 0, next_attempt_at_millis INTEGER NOT NULL, record_json TEXT NOT NULL,
|
|
200
124
|
PRIMARY KEY (tenant_id, source_address, event_id)
|
|
201
125
|
)`.withoutTransform;
|
|
202
126
|
yield* sql`CREATE INDEX effect_agent_subscription_events_pending ON effect_agent_subscription_events (tenant_id, source_address, routing_complete, next_attempt_at_millis, event_id)`
|
|
@@ -238,7 +162,7 @@ const initializeDoSubscriptionStore = Effect.fn("DoSubscriptionStore.initialize"
|
|
|
238
162
|
);
|
|
239
163
|
});
|
|
240
164
|
|
|
241
|
-
const makeSubscriptionStore = Effect.fn("
|
|
165
|
+
const makeSubscriptionStore = Effect.fn("DoSubscriptionStore.make")(function* (
|
|
242
166
|
owned: SourcePartition,
|
|
243
167
|
) {
|
|
244
168
|
const partition = yield* validate(SourcePartition, owned, "partition");
|
|
@@ -247,11 +171,6 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
|
|
|
247
171
|
const transactions = yield* DoSubscriptionTransaction;
|
|
248
172
|
|
|
249
173
|
yield* initializeDoSubscriptionStore();
|
|
250
|
-
yield* sql`
|
|
251
|
-
INSERT INTO effect_agent_subscription_sequences (
|
|
252
|
-
tenant_id, source_address, sequence, event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
|
|
253
|
-
) VALUES (${partition.tenantId}, ${partition.address}, 0, '', '', 0) ON CONFLICT DO NOTHING
|
|
254
|
-
`.pipe(Effect.mapError(() => unavailable("initialize subscription partition")));
|
|
255
174
|
|
|
256
175
|
const query = <A extends object>(
|
|
257
176
|
effect: Effect.Effect<ReadonlyArray<A>, SqlError>,
|
|
@@ -283,7 +202,11 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
|
|
|
283
202
|
SELECT next_attempt_at_millis AS deadline FROM effect_agent_subscription_events
|
|
284
203
|
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND routing_complete=0
|
|
285
204
|
UNION ALL SELECT next_attempt_at_millis FROM effect_agent_subscription_deliveries
|
|
286
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND
|
|
205
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND CASE WHEN json_valid(record_json) THEN
|
|
206
|
+
((state NOT IN ('delivered','refused') AND COALESCE(json_extract(record_json, '$.retry.parked'), 0)=0) OR (state='delivered' AND json_extract(record_json, '$.observeSettlement')=1))
|
|
207
|
+
ELSE state<>'refused' END
|
|
208
|
+
UNION ALL SELECT next_maintenance_at_millis FROM effect_agent_event_retention
|
|
209
|
+
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
287
210
|
UNION ALL SELECT recovery_at_millis FROM effect_agent_subscriptions
|
|
288
211
|
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active' AND recovery_at_millis IS NOT NULL
|
|
289
212
|
)
|
|
@@ -334,900 +257,9 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
|
|
|
334
257
|
}),
|
|
335
258
|
);
|
|
336
259
|
|
|
337
|
-
const
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
: Effect.fail(error("validation", code));
|
|
341
|
-
|
|
342
|
-
const requireKey = Effect.fn("SqliteSubscriptionStore.requireKey")(function* (
|
|
343
|
-
input: SubscriptionKey,
|
|
344
|
-
code: string,
|
|
345
|
-
) {
|
|
346
|
-
const key = yield* validate(SubscriptionKey, input, code);
|
|
347
|
-
|
|
348
|
-
yield* requirePartition(key.partition, code);
|
|
349
|
-
|
|
350
|
-
return key;
|
|
351
|
-
});
|
|
352
|
-
|
|
353
|
-
const readRegistration = Effect.fn("SqliteSubscriptionStore.readRegistration")(function* (
|
|
354
|
-
key: SubscriptionKey,
|
|
355
|
-
code: string,
|
|
356
|
-
) {
|
|
357
|
-
const rows = yield* query(
|
|
358
|
-
sql<Record<string, unknown>>`
|
|
359
|
-
SELECT owner_id, subscription_id, ordinal, source_name, source_version, matching_key, state,
|
|
360
|
-
expires_at_millis, recovery_at_millis, record_json FROM effect_agent_subscriptions
|
|
361
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
362
|
-
AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId}
|
|
363
|
-
`,
|
|
364
|
-
code,
|
|
365
|
-
);
|
|
366
|
-
|
|
367
|
-
const decodedRows = yield* decodeRows(RegistrationRow, rows, code);
|
|
368
|
-
|
|
369
|
-
if (decodedRows.length > 1) return yield* corrupt(code);
|
|
370
|
-
const row = decodedRows[0];
|
|
371
|
-
|
|
372
|
-
if (row === undefined) return null;
|
|
373
|
-
const record = yield* decode(SubscriptionRecord, row.record_json, code);
|
|
374
|
-
|
|
375
|
-
if (
|
|
376
|
-
!sameSourcePartition(record.key.partition, partition) ||
|
|
377
|
-
record.key.ownerId !== row.owner_id ||
|
|
378
|
-
record.key.subscriptionId !== row.subscription_id ||
|
|
379
|
-
record.ordinal !== row.ordinal ||
|
|
380
|
-
record.configuration.source.name !== row.source_name ||
|
|
381
|
-
record.configuration.source.version !== row.source_version ||
|
|
382
|
-
record.configuration.matchingKey !== row.matching_key ||
|
|
383
|
-
record.state !== row.state ||
|
|
384
|
-
record.configuration.expiresAtMillis !== row.expires_at_millis ||
|
|
385
|
-
(record.recovery?.nextAttemptAtMillis ?? null) !== row.recovery_at_millis
|
|
386
|
-
)
|
|
387
|
-
return yield* corrupt(`${code}-projection`);
|
|
388
|
-
|
|
389
|
-
return record;
|
|
390
|
-
});
|
|
391
|
-
|
|
392
|
-
const readEvent = Effect.fn("SqliteSubscriptionStore.readEvent")(function* (
|
|
393
|
-
eventId: string,
|
|
394
|
-
code: string,
|
|
395
|
-
) {
|
|
396
|
-
const rows = yield* query(
|
|
397
|
-
sql<Record<string, unknown>>`
|
|
398
|
-
SELECT event_id, source_name, source_version, matching_key, payload_digest, cutoff, cursor,
|
|
399
|
-
routing_complete, next_attempt_at_millis, record_json FROM effect_agent_subscription_events
|
|
400
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${eventId}
|
|
401
|
-
`,
|
|
402
|
-
code,
|
|
403
|
-
);
|
|
404
|
-
|
|
405
|
-
const decodedRows = yield* decodeRows(EventRow, rows, code);
|
|
406
|
-
|
|
407
|
-
if (decodedRows.length > 1) return yield* corrupt(code);
|
|
408
|
-
const row = decodedRows[0];
|
|
409
|
-
|
|
410
|
-
if (row === undefined) return null;
|
|
411
|
-
const event = yield* decode(AcceptedEvent, row.record_json, code);
|
|
412
|
-
|
|
413
|
-
if (
|
|
414
|
-
!sameSourcePartition(event.partition, partition) ||
|
|
415
|
-
event.eventId !== row.event_id ||
|
|
416
|
-
event.source.name !== row.source_name ||
|
|
417
|
-
event.source.version !== row.source_version ||
|
|
418
|
-
event.matchingKey !== row.matching_key ||
|
|
419
|
-
event.payloadDigest !== row.payload_digest ||
|
|
420
|
-
event.cutoff !== row.cutoff ||
|
|
421
|
-
event.cursor !== row.cursor ||
|
|
422
|
-
(event.routingComplete ? 1 : 0) !== row.routing_complete ||
|
|
423
|
-
event.nextAttemptAtMillis !== row.next_attempt_at_millis
|
|
424
|
-
)
|
|
425
|
-
return yield* corrupt(`${code}-projection`);
|
|
426
|
-
|
|
427
|
-
return event;
|
|
428
|
-
});
|
|
429
|
-
|
|
430
|
-
const readDelivery = Effect.fn("SqliteSubscriptionStore.readDelivery")(function* (
|
|
431
|
-
key: SubscriptionDeliveryKey,
|
|
432
|
-
code: string,
|
|
433
|
-
) {
|
|
434
|
-
const rows = yield* query(
|
|
435
|
-
sql<Record<string, unknown>>`
|
|
436
|
-
SELECT owner_id, subscription_id, event_id, delivery_key, state, next_attempt_at_millis, record_json
|
|
437
|
-
FROM effect_agent_subscription_deliveries
|
|
438
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
439
|
-
AND owner_id=${key.subscription.ownerId} AND subscription_id=${key.subscription.subscriptionId}
|
|
440
|
-
AND event_id=${key.eventId}
|
|
441
|
-
`,
|
|
442
|
-
code,
|
|
443
|
-
);
|
|
444
|
-
|
|
445
|
-
const decodedRows = yield* decodeRows(DeliveryRow, rows, code);
|
|
446
|
-
|
|
447
|
-
if (decodedRows.length > 1) return yield* corrupt(code);
|
|
448
|
-
const row = decodedRows[0];
|
|
449
|
-
|
|
450
|
-
if (row === undefined) return null;
|
|
451
|
-
const delivery = yield* decode(SubscriptionDelivery, row.record_json, code);
|
|
452
|
-
|
|
453
|
-
if (
|
|
454
|
-
!sameSourcePartition(delivery.key.subscription.partition, partition) ||
|
|
455
|
-
delivery.key.subscription.ownerId !== row.owner_id ||
|
|
456
|
-
delivery.key.subscription.subscriptionId !== row.subscription_id ||
|
|
457
|
-
delivery.key.eventId !== row.event_id ||
|
|
458
|
-
subscriptionDeliveryKeyString(delivery.key) !== row.delivery_key ||
|
|
459
|
-
delivery.state !== row.state ||
|
|
460
|
-
delivery.retry.nextAttemptAtMillis !== row.next_attempt_at_millis
|
|
461
|
-
)
|
|
462
|
-
return yield* corrupt(`${code}-projection`);
|
|
463
|
-
|
|
464
|
-
return delivery;
|
|
465
|
-
});
|
|
466
|
-
|
|
467
|
-
const count = Effect.fn("SqliteSubscriptionStore.count")(function* (
|
|
468
|
-
statement: Effect.Effect<ReadonlyArray<Record<string, unknown>>, SqlError>,
|
|
469
|
-
code: string,
|
|
470
|
-
) {
|
|
471
|
-
const rows = yield* query(statement, code);
|
|
472
|
-
const decoded = yield* decodeRows(CountRow, rows, code);
|
|
473
|
-
|
|
474
|
-
if (decoded.length !== 1) return yield* corrupt(code);
|
|
475
|
-
|
|
476
|
-
return decoded[0].count;
|
|
477
|
-
});
|
|
478
|
-
|
|
479
|
-
const nextSequence = Effect.fn("SqliteSubscriptionStore.nextSequence")(function* () {
|
|
480
|
-
const rows = yield* query(
|
|
481
|
-
sql<Record<string, unknown>>`
|
|
482
|
-
UPDATE effect_agent_subscription_sequences SET sequence=sequence+1
|
|
483
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
484
|
-
RETURNING sequence
|
|
485
|
-
`,
|
|
486
|
-
"advance subscription sequence",
|
|
487
|
-
);
|
|
488
|
-
|
|
489
|
-
const decoded = yield* decodeRows(SequenceRow, rows, "advance subscription sequence");
|
|
490
|
-
|
|
491
|
-
if (decoded.length !== 1) return yield* corrupt("subscription sequence");
|
|
492
|
-
|
|
493
|
-
return decoded[0].sequence;
|
|
494
|
-
});
|
|
495
|
-
|
|
496
|
-
const writeRegistration = Effect.fn("SqliteSubscriptionStore.writeRegistration")(function* (
|
|
497
|
-
record: SubscriptionRecord,
|
|
498
|
-
) {
|
|
499
|
-
const json = yield* encode(SubscriptionRecord, record, "encode subscription");
|
|
500
|
-
|
|
501
|
-
yield* query(
|
|
502
|
-
sql<Record<string, unknown>>`
|
|
503
|
-
UPDATE effect_agent_subscriptions SET state=${record.state}, expires_at_millis=${record.configuration.expiresAtMillis},
|
|
504
|
-
recovery_at_millis=${record.recovery?.nextAttemptAtMillis ?? null}, record_json=${json}
|
|
505
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
506
|
-
AND owner_id=${record.key.ownerId} AND subscription_id=${record.key.subscriptionId}
|
|
507
|
-
`,
|
|
508
|
-
"write subscription",
|
|
509
|
-
);
|
|
510
|
-
});
|
|
511
|
-
|
|
512
|
-
const writeEvent = Effect.fn("SqliteSubscriptionStore.writeEvent")(function* (
|
|
513
|
-
event: AcceptedEvent,
|
|
514
|
-
) {
|
|
515
|
-
const json = yield* encode(AcceptedEvent, event, "encode event");
|
|
516
|
-
|
|
517
|
-
yield* query(
|
|
518
|
-
sql<Record<string, unknown>>`
|
|
519
|
-
UPDATE effect_agent_subscription_events SET cursor=${event.cursor}, routing_complete=${event.routingComplete ? 1 : 0},
|
|
520
|
-
next_attempt_at_millis=${event.nextAttemptAtMillis}, record_json=${json}
|
|
521
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND event_id=${event.eventId}
|
|
522
|
-
`,
|
|
523
|
-
"write event",
|
|
524
|
-
);
|
|
525
|
-
});
|
|
526
|
-
|
|
527
|
-
const writeDelivery = Effect.fn("SqliteSubscriptionStore.writeDelivery")(function* (
|
|
528
|
-
delivery: SubscriptionDelivery,
|
|
529
|
-
) {
|
|
530
|
-
const json = yield* encode(SubscriptionDelivery, delivery, "encode delivery");
|
|
531
|
-
|
|
532
|
-
yield* query(
|
|
533
|
-
sql<Record<string, unknown>>`
|
|
534
|
-
UPDATE effect_agent_subscription_deliveries SET state=${delivery.state},
|
|
535
|
-
next_attempt_at_millis=${delivery.retry.nextAttemptAtMillis}, record_json=${json}
|
|
536
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
537
|
-
AND owner_id=${delivery.key.subscription.ownerId} AND subscription_id=${delivery.key.subscription.subscriptionId}
|
|
538
|
-
AND event_id=${delivery.key.eventId}
|
|
539
|
-
`,
|
|
540
|
-
"write delivery",
|
|
541
|
-
);
|
|
542
|
-
});
|
|
543
|
-
|
|
544
|
-
const register: SubscriptionStore["Service"]["register"] = Effect.fn(
|
|
545
|
-
"SqliteSubscriptionStore.register",
|
|
546
|
-
)(function* (input, inputLimits) {
|
|
547
|
-
const record = yield* validate(SubscriptionRecord, input, "register-record");
|
|
548
|
-
const limits = yield* validate(SubscriptionLimits, inputLimits, "register-limits");
|
|
549
|
-
|
|
550
|
-
yield* requirePartition(record.key.partition, "register-partition");
|
|
551
|
-
|
|
552
|
-
const result = yield* transact(
|
|
553
|
-
Effect.gen(function* () {
|
|
554
|
-
const existing = yield* readRegistration(record.key, "register-existing");
|
|
555
|
-
|
|
556
|
-
if (existing !== null) {
|
|
557
|
-
if (existing.creationFingerprint !== record.creationFingerprint)
|
|
558
|
-
return yield* error("conflict", "registration-identity");
|
|
559
|
-
|
|
560
|
-
return { value: existing, changed: false } as const;
|
|
561
|
-
}
|
|
562
|
-
if (bytes(record.configuration.context) > limits.maxContextBytes)
|
|
563
|
-
return yield* error("capacity", "context-bytes");
|
|
564
|
-
if (bytes(record.configuration.parameters) > limits.maxPayloadBytes)
|
|
565
|
-
return yield* error("capacity", "parameters-bytes");
|
|
566
|
-
if (
|
|
567
|
-
record.configuration.expiresAtMillis - record.createdAtMillis >
|
|
568
|
-
limits.maxLifetimeMillis
|
|
569
|
-
)
|
|
570
|
-
return yield* error("capacity", "lifetime");
|
|
571
|
-
if (
|
|
572
|
-
(yield* count(
|
|
573
|
-
sql<
|
|
574
|
-
Record<string, unknown>
|
|
575
|
-
>`SELECT COUNT(*) AS count FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`,
|
|
576
|
-
"count registrations",
|
|
577
|
-
)) >= limits.maxRegistrations
|
|
578
|
-
)
|
|
579
|
-
return yield* error("capacity", "registrations");
|
|
580
|
-
if (
|
|
581
|
-
(yield* count(
|
|
582
|
-
sql<
|
|
583
|
-
Record<string, unknown>
|
|
584
|
-
>`SELECT COUNT(*) AS count FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND owner_id=${record.key.ownerId}`,
|
|
585
|
-
"count owner registrations",
|
|
586
|
-
)) >= limits.maxRegistrationsPerOwner
|
|
587
|
-
)
|
|
588
|
-
return yield* error("capacity", "owner-registrations");
|
|
589
|
-
const assigned = { ...record, ordinal: yield* nextSequence() };
|
|
590
|
-
const json = yield* encode(SubscriptionRecord, assigned, "encode registration");
|
|
591
|
-
|
|
592
|
-
yield* failpoint.hit("subscription:register:before");
|
|
593
|
-
yield* query(
|
|
594
|
-
sql<Record<string, unknown>>`
|
|
595
|
-
INSERT INTO effect_agent_subscriptions (tenant_id, source_address, owner_id, subscription_id, ordinal,
|
|
596
|
-
source_name, source_version, matching_key, state, expires_at_millis, recovery_at_millis, record_json)
|
|
597
|
-
VALUES (${partition.tenantId}, ${partition.address}, ${assigned.key.ownerId}, ${assigned.key.subscriptionId}, ${assigned.ordinal},
|
|
598
|
-
${assigned.configuration.source.name}, ${assigned.configuration.source.version}, ${assigned.configuration.matchingKey}, ${assigned.state},
|
|
599
|
-
${assigned.configuration.expiresAtMillis}, ${assigned.recovery?.nextAttemptAtMillis ?? null}, ${json})
|
|
600
|
-
`,
|
|
601
|
-
"insert registration",
|
|
602
|
-
);
|
|
603
|
-
|
|
604
|
-
return { value: assigned, changed: true } as const;
|
|
605
|
-
}),
|
|
606
|
-
);
|
|
607
|
-
|
|
608
|
-
if (result.changed) yield* failpoint.hit("subscription:register:after");
|
|
609
|
-
|
|
610
|
-
return result.value;
|
|
611
|
-
});
|
|
612
|
-
|
|
613
|
-
const get: SubscriptionStore["Service"]["get"] = Effect.fn("SqliteSubscriptionStore.get")(
|
|
614
|
-
function* (input) {
|
|
615
|
-
return yield* readRegistration(yield* requireKey(input, "get-key"), "get subscription");
|
|
616
|
-
},
|
|
617
|
-
);
|
|
618
|
-
|
|
619
|
-
const list: SubscriptionStore["Service"]["list"] = Effect.fn("SqliteSubscriptionStore.list")(
|
|
620
|
-
function* (ownerId, after, limit) {
|
|
621
|
-
const rows = yield* query(
|
|
622
|
-
sql<Record<string, unknown>>`
|
|
623
|
-
SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
624
|
-
AND owner_id=${ownerId} AND ordinal>${after} ORDER BY ordinal LIMIT ${limit}
|
|
625
|
-
`,
|
|
626
|
-
"list subscriptions",
|
|
627
|
-
);
|
|
628
|
-
|
|
629
|
-
const decoded = yield* decodeRows(
|
|
630
|
-
Schema.Struct({
|
|
631
|
-
owner_id: Schema.String,
|
|
632
|
-
subscription_id: Schema.String,
|
|
633
|
-
ordinal: Schema.Natural,
|
|
634
|
-
}),
|
|
635
|
-
rows,
|
|
636
|
-
"list subscriptions",
|
|
637
|
-
);
|
|
638
|
-
|
|
639
|
-
return yield* Effect.forEach(
|
|
640
|
-
decoded,
|
|
641
|
-
Effect.fn("SqliteSubscriptionStore.listRecord")(function* (row) {
|
|
642
|
-
const record = yield* readRegistration(
|
|
643
|
-
{ partition, ownerId: row.owner_id, subscriptionId: row.subscription_id },
|
|
644
|
-
"list subscription",
|
|
645
|
-
);
|
|
646
|
-
|
|
647
|
-
if (record === null || record.ordinal !== row.ordinal)
|
|
648
|
-
return yield* corrupt("list subscription projection");
|
|
649
|
-
|
|
650
|
-
return record;
|
|
651
|
-
}),
|
|
652
|
-
);
|
|
653
|
-
},
|
|
654
|
-
);
|
|
655
|
-
|
|
656
|
-
const cancel: SubscriptionStore["Service"]["cancel"] = Effect.fn(
|
|
657
|
-
"SqliteSubscriptionStore.cancel",
|
|
658
|
-
)(function* (input) {
|
|
659
|
-
const key = yield* requireKey(input, "cancel-key");
|
|
660
|
-
|
|
661
|
-
const result = yield* transact(
|
|
662
|
-
Effect.gen(function* () {
|
|
663
|
-
const current = yield* readRegistration(key, "cancel subscription");
|
|
664
|
-
|
|
665
|
-
if (current === null) return yield* error("not-found", "subscription");
|
|
666
|
-
if (current.state === "cancelled") return { value: current, changed: false } as const;
|
|
667
|
-
const updated = { ...current, state: "cancelled" as const, recovery: null };
|
|
668
|
-
|
|
669
|
-
yield* failpoint.hit("subscription:cancel:before");
|
|
670
|
-
yield* writeRegistration(updated);
|
|
671
|
-
|
|
672
|
-
return { value: updated, changed: true } as const;
|
|
673
|
-
}),
|
|
674
|
-
);
|
|
675
|
-
|
|
676
|
-
if (result.changed) yield* failpoint.hit("subscription:cancel:after");
|
|
677
|
-
|
|
678
|
-
return result.value;
|
|
679
|
-
});
|
|
680
|
-
|
|
681
|
-
const accept: SubscriptionStore["Service"]["accept"] = Effect.fn(
|
|
682
|
-
"SqliteSubscriptionStore.accept",
|
|
683
|
-
)(function* (input, inputLimits) {
|
|
684
|
-
const event = yield* validate(AcceptedEvent, input, "accept-event");
|
|
685
|
-
const limits = yield* validate(SubscriptionLimits, inputLimits, "accept-limits");
|
|
686
|
-
|
|
687
|
-
yield* requirePartition(event.partition, "accept-partition");
|
|
688
|
-
|
|
689
|
-
const result = yield* transact(
|
|
690
|
-
Effect.gen(function* () {
|
|
691
|
-
const existing = yield* readEvent(event.eventId, "accept event");
|
|
692
|
-
|
|
693
|
-
if (existing !== null) {
|
|
694
|
-
if (!sameAcceptedEventIdentity(existing, event))
|
|
695
|
-
return yield* error("conflict", "event-identity");
|
|
696
|
-
|
|
697
|
-
return { value: existing, changed: false } as const;
|
|
698
|
-
}
|
|
699
|
-
if (bytes(event.payload) > limits.maxPayloadBytes)
|
|
700
|
-
return yield* error("capacity", "payload-bytes");
|
|
701
|
-
if (
|
|
702
|
-
(yield* count(
|
|
703
|
-
sql<
|
|
704
|
-
Record<string, unknown>
|
|
705
|
-
>`SELECT COUNT(*) AS count FROM effect_agent_subscription_events WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`,
|
|
706
|
-
"count events",
|
|
707
|
-
)) >= limits.maxEvents
|
|
708
|
-
)
|
|
709
|
-
return yield* error("capacity", "events");
|
|
710
|
-
|
|
711
|
-
const accepted: AcceptedEvent = {
|
|
712
|
-
...event,
|
|
713
|
-
cutoff: yield* nextSequence(),
|
|
714
|
-
cursor: 0,
|
|
715
|
-
routingComplete: false,
|
|
716
|
-
routingFailure: null,
|
|
717
|
-
};
|
|
718
|
-
|
|
719
|
-
const json = yield* encode(AcceptedEvent, accepted, "encode accepted event");
|
|
720
|
-
|
|
721
|
-
yield* failpoint.hit("subscription:accept:before");
|
|
722
|
-
yield* query(
|
|
723
|
-
sql<Record<string, unknown>>`
|
|
724
|
-
INSERT INTO effect_agent_subscription_events (tenant_id, source_address, event_id, source_name, source_version,
|
|
725
|
-
matching_key, payload_digest, cutoff, cursor, routing_complete, next_attempt_at_millis, record_json)
|
|
726
|
-
VALUES (${partition.tenantId}, ${partition.address}, ${accepted.eventId}, ${accepted.source.name}, ${accepted.source.version},
|
|
727
|
-
${accepted.matchingKey}, ${accepted.payloadDigest}, ${accepted.cutoff}, ${accepted.cursor}, 0, ${accepted.nextAttemptAtMillis}, ${json})
|
|
728
|
-
`,
|
|
729
|
-
"insert event",
|
|
730
|
-
);
|
|
731
|
-
|
|
732
|
-
return { value: accepted, changed: true } as const;
|
|
733
|
-
}),
|
|
734
|
-
);
|
|
735
|
-
|
|
736
|
-
if (result.changed) yield* failpoint.hit("subscription:accept:after");
|
|
737
|
-
|
|
738
|
-
return result.value;
|
|
739
|
-
});
|
|
740
|
-
|
|
741
|
-
const event: SubscriptionStore["Service"]["event"] = Effect.fn("SqliteSubscriptionStore.event")(
|
|
742
|
-
(eventId) => readEvent(eventId, "get event"),
|
|
743
|
-
);
|
|
744
|
-
|
|
745
|
-
const pendingEvents: SubscriptionStore["Service"]["pendingEvents"] = Effect.fn(
|
|
746
|
-
"SqliteSubscriptionStore.pendingEvents",
|
|
747
|
-
)(function* (nowMillis, after, limit) {
|
|
748
|
-
const rows = yield* query(
|
|
749
|
-
sql<Record<string, unknown>>`
|
|
750
|
-
SELECT event_id FROM effect_agent_subscription_events WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
751
|
-
AND routing_complete=0 AND next_attempt_at_millis<=${nowMillis} AND event_id>${after} ORDER BY event_id LIMIT ${limit}
|
|
752
|
-
`,
|
|
753
|
-
"pending events",
|
|
754
|
-
);
|
|
755
|
-
|
|
756
|
-
return yield* decodeRows(
|
|
757
|
-
Schema.Struct({ event_id: Schema.String }),
|
|
758
|
-
rows,
|
|
759
|
-
"pending event keys",
|
|
760
|
-
).pipe(Effect.map((items) => items.map((item) => item.event_id)));
|
|
761
|
-
});
|
|
762
|
-
|
|
763
|
-
const candidates: SubscriptionStore["Service"]["candidates"] = Effect.fn(
|
|
764
|
-
"SqliteSubscriptionStore.candidates",
|
|
765
|
-
)(function* (input, limit) {
|
|
766
|
-
const supplied = yield* validate(AcceptedEvent, input, "candidates-event");
|
|
767
|
-
|
|
768
|
-
yield* requirePartition(supplied.partition, "candidates-partition");
|
|
769
|
-
const stored = yield* readEvent(supplied.eventId, "candidates event");
|
|
770
|
-
|
|
771
|
-
if (stored === null) return yield* error("not-found", "event");
|
|
772
|
-
if (!sameAcceptedEventIdentity(stored, supplied)) return yield* error("conflict", "event");
|
|
773
|
-
|
|
774
|
-
const rows = yield* query(
|
|
775
|
-
sql<Record<string, unknown>>`
|
|
776
|
-
SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
777
|
-
AND source_name=${stored.source.name} AND source_version=${stored.source.version} AND matching_key=${stored.matchingKey}
|
|
778
|
-
AND ordinal>${stored.cursor} AND ordinal<=${stored.cutoff} ORDER BY ordinal LIMIT ${limit}
|
|
779
|
-
`,
|
|
780
|
-
"subscription candidates",
|
|
781
|
-
);
|
|
782
|
-
|
|
783
|
-
const decoded = yield* decodeRows(
|
|
784
|
-
Schema.Struct({
|
|
785
|
-
owner_id: Schema.String,
|
|
786
|
-
subscription_id: Schema.String,
|
|
787
|
-
ordinal: Schema.Natural,
|
|
788
|
-
}),
|
|
789
|
-
rows,
|
|
790
|
-
"subscription candidates",
|
|
791
|
-
);
|
|
792
|
-
|
|
793
|
-
return yield* Effect.forEach(
|
|
794
|
-
decoded,
|
|
795
|
-
Effect.fn("SqliteSubscriptionStore.candidateRecord")(function* (row) {
|
|
796
|
-
const record = yield* readRegistration(
|
|
797
|
-
{ partition, ownerId: row.owner_id, subscriptionId: row.subscription_id },
|
|
798
|
-
"subscription candidate",
|
|
799
|
-
);
|
|
800
|
-
|
|
801
|
-
if (record === null || record.ordinal !== row.ordinal)
|
|
802
|
-
return yield* corrupt("subscription candidate projection");
|
|
803
|
-
|
|
804
|
-
return record;
|
|
805
|
-
}),
|
|
806
|
-
);
|
|
807
|
-
});
|
|
808
|
-
|
|
809
|
-
const insertDelivery = Effect.fn("SqliteSubscriptionStore.insertDelivery")(function* (
|
|
810
|
-
delivery: SubscriptionDelivery,
|
|
811
|
-
) {
|
|
812
|
-
const json = yield* encode(SubscriptionDelivery, delivery, "encode selected delivery");
|
|
813
|
-
|
|
814
|
-
yield* query(
|
|
815
|
-
sql<Record<string, unknown>>`
|
|
816
|
-
INSERT INTO effect_agent_subscription_deliveries (tenant_id, source_address, owner_id, subscription_id, event_id,
|
|
817
|
-
delivery_key, state, next_attempt_at_millis, record_json)
|
|
818
|
-
VALUES (${partition.tenantId}, ${partition.address}, ${delivery.key.subscription.ownerId}, ${delivery.key.subscription.subscriptionId},
|
|
819
|
-
${delivery.key.eventId}, ${subscriptionDeliveryKeyString(delivery.key)}, ${delivery.state}, ${delivery.retry.nextAttemptAtMillis}, ${json})
|
|
820
|
-
`,
|
|
821
|
-
"insert delivery",
|
|
822
|
-
);
|
|
823
|
-
});
|
|
824
|
-
|
|
825
|
-
const select: SubscriptionStore["Service"]["select"] = Effect.fn(
|
|
826
|
-
"SqliteSubscriptionStore.select",
|
|
827
|
-
)(function* (inputEvent, inputDeliveries, cursor, complete, nowMillis, inputLimits) {
|
|
828
|
-
const supplied = yield* validate(AcceptedEvent, inputEvent, "select-event");
|
|
829
|
-
|
|
830
|
-
const deliveries = yield* validate(
|
|
831
|
-
Schema.Array(SubscriptionDelivery),
|
|
832
|
-
inputDeliveries,
|
|
833
|
-
"select-deliveries",
|
|
834
|
-
);
|
|
835
|
-
|
|
836
|
-
const limits = yield* validate(SubscriptionLimits, inputLimits, "select-limits");
|
|
837
|
-
|
|
838
|
-
yield* requirePartition(supplied.partition, "select-partition");
|
|
839
|
-
for (const candidate of deliveries)
|
|
840
|
-
yield* requirePartition(candidate.key.subscription.partition, "select-delivery-partition");
|
|
841
|
-
|
|
842
|
-
const changed = yield* transact(
|
|
843
|
-
Effect.gen(function* () {
|
|
844
|
-
const accepted = yield* readEvent(supplied.eventId, "select event");
|
|
845
|
-
|
|
846
|
-
if (accepted === null) return yield* error("not-found", "event");
|
|
847
|
-
if (!sameAcceptedEventIdentity(accepted, supplied) || accepted.cursor !== supplied.cursor)
|
|
848
|
-
return yield* error("conflict", "event-cursor");
|
|
849
|
-
if (accepted.routingComplete) return false;
|
|
850
|
-
if (!Number.isSafeInteger(cursor) || cursor < accepted.cursor || cursor > accepted.cutoff)
|
|
851
|
-
return yield* error("validation", "cursor");
|
|
852
|
-
yield* failpoint.hit("subscription:select:before");
|
|
853
|
-
const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
|
|
854
|
-
const additions: Array<{ delivery: SubscriptionDelivery; record: SubscriptionRecord }> = [];
|
|
855
|
-
|
|
856
|
-
for (const delivery of deliveries) {
|
|
857
|
-
const record = yield* readRegistration(delivery.key.subscription, "select registration");
|
|
858
|
-
|
|
859
|
-
if (record === null) return yield* error("not-found", "subscription");
|
|
860
|
-
if (
|
|
861
|
-
!subscriptionDeliveryCanSelect(delivery, record, accepted) ||
|
|
862
|
-
delivery.key.eventId !== accepted.eventId ||
|
|
863
|
-
delivery.source.name !== accepted.source.name ||
|
|
864
|
-
delivery.source.version !== accepted.source.version ||
|
|
865
|
-
record.ordinal <= accepted.cursor ||
|
|
866
|
-
record.ordinal > cursor
|
|
867
|
-
)
|
|
868
|
-
return yield* error("conflict", "selection");
|
|
869
|
-
const existing = yield* readDelivery(delivery.key, "select existing delivery");
|
|
870
|
-
|
|
871
|
-
if (existing !== null) {
|
|
872
|
-
if (!sameDeliveryIdentity(existing, delivery))
|
|
873
|
-
return yield* error("conflict", "delivery-identity");
|
|
874
|
-
continue;
|
|
875
|
-
}
|
|
876
|
-
if (!subscriptionCanSelect(record, accepted, effectiveNowMillis, false)) continue;
|
|
877
|
-
additions.push({ delivery, record });
|
|
878
|
-
}
|
|
879
|
-
|
|
880
|
-
const total = yield* count(
|
|
881
|
-
sql<
|
|
882
|
-
Record<string, unknown>
|
|
883
|
-
>`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`,
|
|
884
|
-
"count deliveries",
|
|
885
|
-
);
|
|
886
|
-
|
|
887
|
-
if (total + additions.length > limits.maxDeliveries)
|
|
888
|
-
return yield* error("capacity", "deliveries");
|
|
889
|
-
for (const ownerId of new Set(additions.map(({ record }) => record.key.ownerId))) {
|
|
890
|
-
const existing = yield* count(
|
|
891
|
-
sql<
|
|
892
|
-
Record<string, unknown>
|
|
893
|
-
>`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND owner_id=${ownerId}`,
|
|
894
|
-
"count owner deliveries",
|
|
895
|
-
);
|
|
896
|
-
|
|
897
|
-
if (
|
|
898
|
-
existing + additions.filter(({ record }) => record.key.ownerId === ownerId).length >
|
|
899
|
-
limits.maxDeliveriesPerOwner
|
|
900
|
-
)
|
|
901
|
-
return yield* error("capacity", "owner-deliveries");
|
|
902
|
-
}
|
|
903
|
-
for (const addition of additions) {
|
|
904
|
-
yield* insertDelivery(addition.delivery);
|
|
905
|
-
if (addition.record.configuration.mode === "once")
|
|
906
|
-
yield* writeRegistration({ ...addition.record, state: "consumed", recovery: null });
|
|
907
|
-
}
|
|
908
|
-
yield* writeEvent({ ...accepted, cursor, routingComplete: complete, routingFailure: null });
|
|
909
|
-
|
|
910
|
-
return true;
|
|
911
|
-
}),
|
|
912
|
-
);
|
|
913
|
-
|
|
914
|
-
if (changed) yield* failpoint.hit("subscription:select:after");
|
|
915
|
-
});
|
|
916
|
-
|
|
917
|
-
const catchUp: SubscriptionStore["Service"]["catchUp"] = Effect.fn(
|
|
918
|
-
"SqliteSubscriptionStore.catchUp",
|
|
919
|
-
)(function* (inputEvent, inputDelivery, nowMillis, inputLimits) {
|
|
920
|
-
const supplied = yield* validate(AcceptedEvent, inputEvent, "catch-up-event");
|
|
921
|
-
const delivery = yield* validate(SubscriptionDelivery, inputDelivery, "catch-up-delivery");
|
|
922
|
-
const limits = yield* validate(SubscriptionLimits, inputLimits, "catch-up-limits");
|
|
923
|
-
|
|
924
|
-
yield* requirePartition(supplied.partition, "catch-up-partition");
|
|
925
|
-
yield* requirePartition(delivery.key.subscription.partition, "catch-up-delivery-partition");
|
|
926
|
-
|
|
927
|
-
const changed = yield* transact(
|
|
928
|
-
Effect.gen(function* () {
|
|
929
|
-
const accepted = yield* readEvent(supplied.eventId, "catch-up event");
|
|
930
|
-
const record = yield* readRegistration(delivery.key.subscription, "catch-up subscription");
|
|
931
|
-
|
|
932
|
-
if (accepted === null || record === null)
|
|
933
|
-
return yield* error("not-found", accepted === null ? "event" : "subscription");
|
|
934
|
-
if (
|
|
935
|
-
!sameAcceptedEventIdentity(accepted, supplied) ||
|
|
936
|
-
!subscriptionDeliveryCanSelect(delivery, record, accepted) ||
|
|
937
|
-
delivery.key.eventId !== accepted.eventId ||
|
|
938
|
-
delivery.source.name !== accepted.source.name ||
|
|
939
|
-
delivery.source.version !== accepted.source.version ||
|
|
940
|
-
record.configuration.mode !== "once"
|
|
941
|
-
)
|
|
942
|
-
return yield* error("conflict", "catch-up-identity");
|
|
943
|
-
const existing = yield* readDelivery(delivery.key, "catch-up existing delivery");
|
|
944
|
-
|
|
945
|
-
if (existing !== null) {
|
|
946
|
-
if (!sameDeliveryIdentity(existing, delivery))
|
|
947
|
-
return yield* error("conflict", "delivery-identity");
|
|
948
|
-
|
|
949
|
-
return false;
|
|
950
|
-
}
|
|
951
|
-
yield* failpoint.hit("subscription:catch-up:before");
|
|
952
|
-
const effectiveNowMillis = Math.max(nowMillis, yield* Clock.currentTimeMillis);
|
|
953
|
-
|
|
954
|
-
if (!subscriptionCanSelect(record, accepted, effectiveNowMillis, true))
|
|
955
|
-
return yield* error("conflict", "catch-up-eligibility");
|
|
956
|
-
if (
|
|
957
|
-
(yield* count(
|
|
958
|
-
sql<
|
|
959
|
-
Record<string, unknown>
|
|
960
|
-
>`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}`,
|
|
961
|
-
"count deliveries",
|
|
962
|
-
)) >= limits.maxDeliveries
|
|
963
|
-
)
|
|
964
|
-
return yield* error("capacity", "deliveries");
|
|
965
|
-
if (
|
|
966
|
-
(yield* count(
|
|
967
|
-
sql<
|
|
968
|
-
Record<string, unknown>
|
|
969
|
-
>`SELECT COUNT(*) AS count FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND owner_id=${record.key.ownerId}`,
|
|
970
|
-
"count owner deliveries",
|
|
971
|
-
)) >= limits.maxDeliveriesPerOwner
|
|
972
|
-
)
|
|
973
|
-
return yield* error("capacity", "owner-deliveries");
|
|
974
|
-
yield* insertDelivery(delivery);
|
|
975
|
-
yield* writeRegistration({ ...record, state: "consumed", recovery: null });
|
|
976
|
-
|
|
977
|
-
return true;
|
|
978
|
-
}),
|
|
979
|
-
);
|
|
980
|
-
|
|
981
|
-
if (changed) yield* failpoint.hit("subscription:catch-up:after");
|
|
982
|
-
});
|
|
983
|
-
|
|
984
|
-
const deferEvent: SubscriptionStore["Service"]["deferEvent"] = Effect.fn(
|
|
985
|
-
"SqliteSubscriptionStore.deferEvent",
|
|
986
|
-
)(function* (eventId, nextAttemptAtMillis, code) {
|
|
987
|
-
const routingFailure =
|
|
988
|
-
code === undefined
|
|
989
|
-
? "routing-failed"
|
|
990
|
-
: yield* validate(SubscriptionName, code, "routing-failure");
|
|
991
|
-
|
|
992
|
-
yield* transact(
|
|
993
|
-
Effect.gen(function* () {
|
|
994
|
-
const accepted = yield* readEvent(eventId, "defer event");
|
|
995
|
-
|
|
996
|
-
if (accepted === null) return yield* error("not-found", "event");
|
|
997
|
-
yield* failpoint.hit("subscription:defer-event:before");
|
|
998
|
-
yield* writeEvent({ ...accepted, nextAttemptAtMillis, routingFailure });
|
|
999
|
-
}),
|
|
1000
|
-
);
|
|
1001
|
-
yield* failpoint.hit("subscription:defer-event:after");
|
|
1002
|
-
});
|
|
1003
|
-
|
|
1004
|
-
const delivery: SubscriptionStore["Service"]["delivery"] = Effect.fn(
|
|
1005
|
-
"SqliteSubscriptionStore.delivery",
|
|
1006
|
-
)(function* (input) {
|
|
1007
|
-
const key = yield* validate(SubscriptionDeliveryKey, input, "delivery-key");
|
|
1008
|
-
|
|
1009
|
-
yield* requirePartition(key.subscription.partition, "delivery-partition");
|
|
1010
|
-
|
|
1011
|
-
return yield* readDelivery(key, "get delivery");
|
|
1012
|
-
});
|
|
1013
|
-
|
|
1014
|
-
const pendingDeliveries: SubscriptionStore["Service"]["pendingDeliveries"] = Effect.fn(
|
|
1015
|
-
"SqliteSubscriptionStore.pendingDeliveries",
|
|
1016
|
-
)(function* (nowMillis, after, limit) {
|
|
1017
|
-
const rows = yield* query(
|
|
1018
|
-
sql<Record<string, unknown>>`
|
|
1019
|
-
SELECT owner_id, subscription_id, event_id FROM effect_agent_subscription_deliveries
|
|
1020
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
|
|
1021
|
-
AND next_attempt_at_millis<=${nowMillis} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
|
|
1022
|
-
`,
|
|
1023
|
-
"pending deliveries",
|
|
1024
|
-
);
|
|
1025
|
-
|
|
1026
|
-
const rowSchema = Schema.Struct({
|
|
1027
|
-
owner_id: Schema.String,
|
|
1028
|
-
subscription_id: Schema.String,
|
|
1029
|
-
event_id: Schema.String,
|
|
1030
|
-
});
|
|
1031
|
-
|
|
1032
|
-
return yield* decodeRows(rowSchema, rows, "pending delivery keys").pipe(
|
|
1033
|
-
Effect.map((items) =>
|
|
1034
|
-
items.map((item) => ({
|
|
1035
|
-
subscription: { partition, ownerId: item.owner_id, subscriptionId: item.subscription_id },
|
|
1036
|
-
eventId: item.event_id,
|
|
1037
|
-
})),
|
|
1038
|
-
),
|
|
1039
|
-
);
|
|
1040
|
-
});
|
|
1041
|
-
|
|
1042
|
-
const listDeliveries: SubscriptionStore["Service"]["listDeliveries"] = Effect.fn(
|
|
1043
|
-
"SqliteSubscriptionStore.listDeliveries",
|
|
1044
|
-
)(function* (input, after, limit) {
|
|
1045
|
-
const key = yield* requireKey(input, "list-deliveries-key");
|
|
1046
|
-
|
|
1047
|
-
const rows = yield* query(
|
|
1048
|
-
sql<Record<string, unknown>>`
|
|
1049
|
-
SELECT record_json FROM effect_agent_subscription_deliveries WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
1050
|
-
AND owner_id=${key.ownerId} AND subscription_id=${key.subscriptionId} AND delivery_key>${after} ORDER BY delivery_key LIMIT ${limit}
|
|
1051
|
-
`,
|
|
1052
|
-
"list deliveries",
|
|
1053
|
-
);
|
|
1054
|
-
|
|
1055
|
-
const decoded = yield* decodeRows(JsonRow, rows, "list deliveries");
|
|
1056
|
-
|
|
1057
|
-
return yield* Effect.forEach(decoded, (row) =>
|
|
1058
|
-
decode(SubscriptionDelivery, row.record_json, "list delivery"),
|
|
1059
|
-
);
|
|
1060
|
-
});
|
|
1061
|
-
|
|
1062
|
-
const changeDelivery: SubscriptionStore["Service"]["changeDelivery"] = Effect.fn(
|
|
1063
|
-
"SqliteSubscriptionStore.changeDelivery",
|
|
1064
|
-
)(function* (inputKey, inputDeliveryId, inputChange) {
|
|
1065
|
-
const key = yield* validate(SubscriptionDeliveryKey, inputKey, "change-delivery-key");
|
|
1066
|
-
const deliveryId = yield* validate(Digest, inputDeliveryId, "change-delivery-id");
|
|
1067
|
-
const change = yield* validate(DeliveryChange, inputChange, "change-delivery-change");
|
|
1068
|
-
|
|
1069
|
-
yield* requirePartition(key.subscription.partition, "change-delivery-partition");
|
|
1070
|
-
|
|
1071
|
-
const result = yield* transact(
|
|
1072
|
-
Effect.gen(function* () {
|
|
1073
|
-
const existing = yield* readDelivery(key, "change delivery");
|
|
1074
|
-
const record = yield* readRegistration(key.subscription, "change delivery subscription");
|
|
1075
|
-
|
|
1076
|
-
if (existing === null || record === null)
|
|
1077
|
-
return yield* error("not-found", existing === null ? "delivery" : "subscription");
|
|
1078
|
-
yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:before`);
|
|
1079
|
-
|
|
1080
|
-
const effectiveChange =
|
|
1081
|
-
change._tag === "Prepare"
|
|
1082
|
-
? { ...change, nowMillis: Math.max(change.nowMillis, yield* Clock.currentTimeMillis) }
|
|
1083
|
-
: change;
|
|
1084
|
-
|
|
1085
|
-
const transition = applySubscriptionDeliveryChange(
|
|
1086
|
-
existing,
|
|
1087
|
-
record,
|
|
1088
|
-
deliveryId,
|
|
1089
|
-
effectiveChange,
|
|
1090
|
-
);
|
|
1091
|
-
|
|
1092
|
-
if (Result.isFailure(transition)) return yield* transition.failure;
|
|
1093
|
-
if (transition.success === existing) return { value: existing, changed: false } as const;
|
|
1094
|
-
yield* writeDelivery(transition.success);
|
|
1095
|
-
|
|
1096
|
-
return { value: transition.success, changed: true } as const;
|
|
1097
|
-
}),
|
|
1098
|
-
);
|
|
1099
|
-
|
|
1100
|
-
if (result.changed)
|
|
1101
|
-
yield* failpoint.hit(`subscription:delivery-${change._tag.toLowerCase()}:after`);
|
|
1102
|
-
|
|
1103
|
-
return result.value;
|
|
1104
|
-
});
|
|
1105
|
-
|
|
1106
|
-
const recovering: SubscriptionStore["Service"]["recovering"] = Effect.fn(
|
|
1107
|
-
"SqliteSubscriptionStore.recovering",
|
|
1108
|
-
)(function* (nowMillis, after, limit) {
|
|
1109
|
-
const rows = yield* query(
|
|
1110
|
-
sql<Record<string, unknown>>`
|
|
1111
|
-
SELECT owner_id, subscription_id, ordinal FROM effect_agent_subscriptions
|
|
1112
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active'
|
|
1113
|
-
AND recovery_at_millis IS NOT NULL AND recovery_at_millis<=${nowMillis} AND ordinal>${after}
|
|
1114
|
-
ORDER BY ordinal LIMIT ${limit}
|
|
1115
|
-
`,
|
|
1116
|
-
"recovering subscriptions",
|
|
1117
|
-
);
|
|
1118
|
-
|
|
1119
|
-
const rowSchema = Schema.Struct({
|
|
1120
|
-
owner_id: Schema.String,
|
|
1121
|
-
subscription_id: Schema.String,
|
|
1122
|
-
ordinal: Schema.Natural,
|
|
1123
|
-
});
|
|
1124
|
-
|
|
1125
|
-
return yield* decodeRows(rowSchema, rows, "recovering subscription keys").pipe(
|
|
1126
|
-
Effect.map((items) =>
|
|
1127
|
-
items.map((item) => ({
|
|
1128
|
-
key: { partition, ownerId: item.owner_id, subscriptionId: item.subscription_id },
|
|
1129
|
-
ordinal: item.ordinal,
|
|
1130
|
-
})),
|
|
1131
|
-
),
|
|
1132
|
-
);
|
|
1133
|
-
});
|
|
1134
|
-
|
|
1135
|
-
const deferRecovery: SubscriptionStore["Service"]["deferRecovery"] = Effect.fn(
|
|
1136
|
-
"SqliteSubscriptionStore.deferRecovery",
|
|
1137
|
-
)(function* (input, recovery) {
|
|
1138
|
-
const key = yield* requireKey(input, "defer-recovery-key");
|
|
1139
|
-
|
|
1140
|
-
yield* transact(
|
|
1141
|
-
Effect.gen(function* () {
|
|
1142
|
-
const record = yield* readRegistration(key, "defer recovery");
|
|
1143
|
-
|
|
1144
|
-
if (record === null) return yield* error("not-found", "subscription");
|
|
1145
|
-
yield* failpoint.hit("subscription:defer-recovery:before");
|
|
1146
|
-
yield* writeRegistration({
|
|
1147
|
-
...record,
|
|
1148
|
-
recovery: record.state === "active" ? recovery : null,
|
|
1149
|
-
});
|
|
1150
|
-
}),
|
|
1151
|
-
);
|
|
1152
|
-
yield* failpoint.hit("subscription:defer-recovery:after");
|
|
1153
|
-
});
|
|
1154
|
-
|
|
1155
|
-
const readScanCursors: SubscriptionStore["Service"]["readScanCursors"] = Effect.gen(function* () {
|
|
1156
|
-
const rows = yield* query(
|
|
1157
|
-
sql<Record<string, unknown>>`
|
|
1158
|
-
SELECT event_scan_cursor, delivery_scan_cursor, recovery_scan_cursor
|
|
1159
|
-
FROM effect_agent_subscription_sequences
|
|
1160
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
1161
|
-
`,
|
|
1162
|
-
"read subscription scan cursors",
|
|
1163
|
-
);
|
|
1164
|
-
|
|
1165
|
-
const decoded = yield* decodeRows(ScanRow, rows, "read subscription scan cursors");
|
|
1166
|
-
|
|
1167
|
-
if (decoded.length !== 1) return yield* corrupt("subscription scan cursors");
|
|
1168
|
-
|
|
1169
|
-
return {
|
|
1170
|
-
events: decoded[0].event_scan_cursor,
|
|
1171
|
-
deliveries: decoded[0].delivery_scan_cursor,
|
|
1172
|
-
recovery: decoded[0].recovery_scan_cursor,
|
|
1173
|
-
};
|
|
1174
|
-
});
|
|
1175
|
-
|
|
1176
|
-
const advanceScanCursors: SubscriptionStore["Service"]["advanceScanCursors"] = Effect.fn(
|
|
1177
|
-
"SqliteSubscriptionStore.advanceScanCursors",
|
|
1178
|
-
)(function* (input) {
|
|
1179
|
-
const cursors = yield* validate(SubscriptionScanCursors, input, "scan-cursors");
|
|
1180
|
-
|
|
1181
|
-
yield* transact(
|
|
1182
|
-
Effect.gen(function* () {
|
|
1183
|
-
yield* failpoint.hit("subscription:advance-scan-cursors:before");
|
|
1184
|
-
yield* query(
|
|
1185
|
-
sql<Record<string, unknown>>`
|
|
1186
|
-
UPDATE effect_agent_subscription_sequences
|
|
1187
|
-
SET event_scan_cursor=${cursors.events}, delivery_scan_cursor=${cursors.deliveries}, recovery_scan_cursor=${cursors.recovery}
|
|
1188
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address}
|
|
1189
|
-
`,
|
|
1190
|
-
"advance subscription scan cursors",
|
|
1191
|
-
);
|
|
1192
|
-
}),
|
|
1193
|
-
);
|
|
1194
|
-
yield* failpoint.hit("subscription:advance-scan-cursors:after");
|
|
1195
|
-
});
|
|
1196
|
-
|
|
1197
|
-
const indexedDeadline = query(
|
|
1198
|
-
sql<Record<string, unknown>>`
|
|
1199
|
-
SELECT MIN(deadline) AS deadline FROM (
|
|
1200
|
-
SELECT next_attempt_at_millis AS deadline FROM effect_agent_subscription_events
|
|
1201
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND routing_complete=0
|
|
1202
|
-
UNION ALL SELECT next_attempt_at_millis FROM effect_agent_subscription_deliveries
|
|
1203
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state NOT IN ('delivered','refused')
|
|
1204
|
-
UNION ALL SELECT recovery_at_millis FROM effect_agent_subscriptions
|
|
1205
|
-
WHERE tenant_id=${partition.tenantId} AND source_address=${partition.address} AND state='active' AND recovery_at_millis IS NOT NULL
|
|
1206
|
-
)
|
|
1207
|
-
`,
|
|
1208
|
-
"next subscription deadline",
|
|
1209
|
-
).pipe(
|
|
1210
|
-
Effect.flatMap((rows) =>
|
|
1211
|
-
decodeRows(
|
|
1212
|
-
Schema.Struct({ deadline: Schema.NullOr(Schema.Number) }),
|
|
1213
|
-
rows,
|
|
1214
|
-
"next subscription deadline",
|
|
1215
|
-
),
|
|
1216
|
-
),
|
|
1217
|
-
Effect.flatMap((rows) =>
|
|
1218
|
-
rows.length === 1
|
|
1219
|
-
? Effect.succeed(rows[0].deadline)
|
|
1220
|
-
: Effect.fail(corrupt("next subscription deadline")),
|
|
1221
|
-
),
|
|
1222
|
-
);
|
|
1223
|
-
|
|
1224
|
-
const nextDeadline = Effect.gen(function* () {
|
|
1225
|
-
const cursors = yield* readScanCursors;
|
|
1226
|
-
|
|
1227
|
-
if (cursors.events !== "" || cursors.deliveries !== "" || cursors.recovery !== 0) return 0;
|
|
1228
|
-
|
|
1229
|
-
return yield* indexedDeadline;
|
|
1230
|
-
});
|
|
260
|
+
const store = yield* makeSqlSubscriptionStore(partition, {
|
|
261
|
+
maxStoredJsonLength: 1_900_000,
|
|
262
|
+
}).pipe(Effect.provide(Layer.succeed(SqlSubscriptionTransaction)({ run: transact })));
|
|
1231
263
|
|
|
1232
264
|
const prearm = Effect.fn("DoSubscriptionStore.prearm")(function* (deadlineAtMillis: number) {
|
|
1233
265
|
yield* transactions.run((replace) => replaceAlarm(replace, deadlineAtMillis));
|
|
@@ -1243,29 +275,9 @@ const makeSubscriptionStore = Effect.fn("SqliteSubscriptionStore.make")(function
|
|
|
1243
275
|
yield* failpoint.hit("subscription:reconcile:after");
|
|
1244
276
|
});
|
|
1245
277
|
|
|
1246
|
-
return Context.make(SubscriptionStore,
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
get,
|
|
1250
|
-
list,
|
|
1251
|
-
cancel,
|
|
1252
|
-
accept,
|
|
1253
|
-
event,
|
|
1254
|
-
pendingEvents,
|
|
1255
|
-
candidates,
|
|
1256
|
-
select,
|
|
1257
|
-
catchUp,
|
|
1258
|
-
deferEvent,
|
|
1259
|
-
delivery,
|
|
1260
|
-
pendingDeliveries,
|
|
1261
|
-
listDeliveries,
|
|
1262
|
-
changeDelivery,
|
|
1263
|
-
recovering,
|
|
1264
|
-
deferRecovery,
|
|
1265
|
-
readScanCursors,
|
|
1266
|
-
advanceScanCursors,
|
|
1267
|
-
nextDeadline,
|
|
1268
|
-
}).pipe(Context.add(DoSubscriptionAlarmControl, { prearm, reconcile }));
|
|
278
|
+
return Context.make(SubscriptionStore, store).pipe(
|
|
279
|
+
Context.add(DoSubscriptionAlarmControl, { prearm, reconcile }),
|
|
280
|
+
);
|
|
1269
281
|
});
|
|
1270
282
|
|
|
1271
283
|
export const doSubscriptionStoreLayer = (
|