effect-actors 0.0.1
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/LICENSE +21 -0
- package/README.md +467 -0
- package/dist/actor/client.d.ts +11 -0
- package/dist/actor/client.js +68 -0
- package/dist/actor/definition.d.ts +77 -0
- package/dist/actor/definition.js +110 -0
- package/dist/actor/state/codec.d.ts +10 -0
- package/dist/actor/state/codec.js +117 -0
- package/dist/actor/state/compiled.d.ts +17 -0
- package/dist/actor/state/compiled.js +1 -0
- package/dist/actor/state/layer.d.ts +7 -0
- package/dist/actor/state/layer.js +167 -0
- package/dist/client/http/errors.d.ts +30 -0
- package/dist/client/http/errors.js +72 -0
- package/dist/client/http/events.d.ts +10 -0
- package/dist/client/http/events.js +30 -0
- package/dist/client/http/layer.d.ts +5 -0
- package/dist/client/http/layer.js +133 -0
- package/dist/client/http.d.ts +1 -0
- package/dist/client/http.js +1 -0
- package/dist/client/outcome.d.ts +4 -0
- package/dist/client/outcome.js +10 -0
- package/dist/client/service.d.ts +28 -0
- package/dist/client/service.js +4 -0
- package/dist/errors/index.d.ts +93 -0
- package/dist/errors/index.js +93 -0
- package/dist/gateway/access.d.ts +11 -0
- package/dist/gateway/access.js +38 -0
- package/dist/gateway/dispatch.d.ts +13 -0
- package/dist/gateway/dispatch.js +45 -0
- package/dist/gateway/fault.d.ts +32 -0
- package/dist/gateway/fault.js +85 -0
- package/dist/gateway/http/events.d.ts +8 -0
- package/dist/gateway/http/events.js +32 -0
- package/dist/gateway/http/response.d.ts +11 -0
- package/dist/gateway/http/response.js +57 -0
- package/dist/gateway/http/routes.d.ts +6 -0
- package/dist/gateway/http/routes.js +184 -0
- package/dist/gateway/http/validation.d.ts +19 -0
- package/dist/gateway/http/validation.js +92 -0
- package/dist/gateway/options.d.ts +31 -0
- package/dist/gateway/options.js +107 -0
- package/dist/gateway/security.d.ts +25 -0
- package/dist/gateway/security.js +4 -0
- package/dist/gateway/service.d.ts +113 -0
- package/dist/gateway/service.js +5 -0
- package/dist/gateway/websocket/operations.d.ts +20 -0
- package/dist/gateway/websocket/operations.js +96 -0
- package/dist/gateway/websocket.d.ts +8 -0
- package/dist/gateway/websocket.js +96 -0
- package/dist/gateway/wire.d.ts +172 -0
- package/dist/gateway/wire.js +102 -0
- package/dist/host/client.d.ts +9 -0
- package/dist/host/client.js +46 -0
- package/dist/host/definition.d.ts +71 -0
- package/dist/host/definition.js +68 -0
- package/dist/host/entity.d.ts +9 -0
- package/dist/host/entity.js +302 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +9 -0
- package/dist/protocol/encoding.d.ts +29 -0
- package/dist/protocol/encoding.js +165 -0
- package/dist/protocol/index.d.ts +90 -0
- package/dist/protocol/index.js +1 -0
- package/dist/protocol/schemas.d.ts +49 -0
- package/dist/protocol/schemas.js +30 -0
- package/dist/runtime/cluster/address.d.ts +3 -0
- package/dist/runtime/cluster/address.js +21 -0
- package/dist/runtime/cluster/client.d.ts +9 -0
- package/dist/runtime/cluster/client.js +131 -0
- package/dist/runtime/cluster/protocol.d.ts +104 -0
- package/dist/runtime/cluster/protocol.js +69 -0
- package/dist/runtime/configuration.d.ts +4 -0
- package/dist/runtime/configuration.js +34 -0
- package/dist/runtime/dispatchers/layer.d.ts +6 -0
- package/dist/runtime/dispatchers/layer.js +53 -0
- package/dist/runtime/dispatchers/outbox.d.ts +4 -0
- package/dist/runtime/dispatchers/outbox.js +57 -0
- package/dist/runtime/layer.d.ts +6 -0
- package/dist/runtime/layer.js +43 -0
- package/dist/runtime/service.d.ts +40 -0
- package/dist/runtime/service.js +5 -0
- package/dist/store/memory/background.d.ts +39 -0
- package/dist/store/memory/background.js +206 -0
- package/dist/store/memory/model.d.ts +72 -0
- package/dist/store/memory/model.js +149 -0
- package/dist/store/memory/ownership.d.ts +11 -0
- package/dist/store/memory/ownership.js +82 -0
- package/dist/store/memory/service.d.ts +3 -0
- package/dist/store/memory/service.js +32 -0
- package/dist/store/memory/state.d.ts +13 -0
- package/dist/store/memory/state.js +109 -0
- package/dist/store/service.d.ts +89 -0
- package/dist/store/service.js +9 -0
- package/dist/store/sql/events.d.ts +13 -0
- package/dist/store/sql/events.js +89 -0
- package/dist/store/sql/model.d.ts +39 -0
- package/dist/store/sql/model.js +126 -0
- package/dist/store/sql/outbox.d.ts +15 -0
- package/dist/store/sql/outbox.js +130 -0
- package/dist/store/sql/ownership.d.ts +44 -0
- package/dist/store/sql/ownership.js +130 -0
- package/dist/store/sql/receipts.d.ts +25 -0
- package/dist/store/sql/receipts.js +93 -0
- package/dist/store/sql/schema.d.ts +9 -0
- package/dist/store/sql/schema.js +124 -0
- package/dist/store/sql/service.d.ts +5 -0
- package/dist/store/sql/service.js +42 -0
- package/dist/store/sql/state.d.ts +13 -0
- package/dist/store/sql/state.js +109 -0
- package/dist/store/sql/timers.d.ts +10 -0
- package/dist/store/sql/timers.js +90 -0
- package/dist/store/sql/transaction.d.ts +6 -0
- package/dist/store/sql/transaction.js +57 -0
- package/dist/store/sql/transition.d.ts +15 -0
- package/dist/store/sql/transition.js +154 -0
- package/dist/testing/index.d.ts +6 -0
- package/dist/testing/index.js +27 -0
- package/package.json +80 -0
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
3
|
+
import { InvalidInput, UnsupportedVersion } from "../../errors/index.js";
|
|
4
|
+
import { dialectOf, integer, normalizeSqlError, protectSql, protocolBoundary, validateTablePrefix, } from "./model.js";
|
|
5
|
+
export const schemaVersion = 1;
|
|
6
|
+
const migrationStatements = (tables) => [
|
|
7
|
+
`CREATE TABLE IF NOT EXISTS ${tables.heads} (
|
|
8
|
+
address TEXT PRIMARY KEY,
|
|
9
|
+
address_json TEXT NOT NULL,
|
|
10
|
+
namespace TEXT NOT NULL,
|
|
11
|
+
fence BIGINT NOT NULL DEFAULT 0 CHECK (fence >= 0),
|
|
12
|
+
lease_until_ms BIGINT NOT NULL DEFAULT 0,
|
|
13
|
+
schema_version INTEGER,
|
|
14
|
+
state TEXT,
|
|
15
|
+
revision BIGINT NOT NULL DEFAULT 0 CHECK (revision >= 0),
|
|
16
|
+
event_sequence BIGINT NOT NULL DEFAULT 0 CHECK (event_sequence >= 0),
|
|
17
|
+
event_floor BIGINT NOT NULL DEFAULT 0 CHECK (event_floor >= 0 AND event_floor <= event_sequence),
|
|
18
|
+
CHECK ((schema_version IS NULL AND state IS NULL) OR (schema_version IS NOT NULL AND state IS NOT NULL))
|
|
19
|
+
)`,
|
|
20
|
+
`CREATE INDEX IF NOT EXISTS ${tables.heads}_namespace_idx ON ${tables.heads} (namespace, address)`,
|
|
21
|
+
`CREATE TABLE IF NOT EXISTS ${tables.receipts} (
|
|
22
|
+
address TEXT NOT NULL,
|
|
23
|
+
command_id TEXT NOT NULL,
|
|
24
|
+
fingerprint TEXT NOT NULL,
|
|
25
|
+
outcome TEXT NOT NULL,
|
|
26
|
+
revision BIGINT NOT NULL CHECK (revision >= 0),
|
|
27
|
+
event_sequence BIGINT NOT NULL CHECK (event_sequence >= 0),
|
|
28
|
+
committed_at_ms BIGINT NOT NULL,
|
|
29
|
+
PRIMARY KEY (address, command_id),
|
|
30
|
+
FOREIGN KEY (address) REFERENCES ${tables.heads} (address) ON DELETE CASCADE
|
|
31
|
+
)`,
|
|
32
|
+
`CREATE TABLE IF NOT EXISTS ${tables.events} (
|
|
33
|
+
address TEXT NOT NULL,
|
|
34
|
+
namespace TEXT NOT NULL,
|
|
35
|
+
sequence BIGINT NOT NULL CHECK (sequence > 0),
|
|
36
|
+
command_id TEXT NOT NULL,
|
|
37
|
+
payload TEXT NOT NULL,
|
|
38
|
+
committed_at_ms BIGINT NOT NULL,
|
|
39
|
+
PRIMARY KEY (address, sequence),
|
|
40
|
+
FOREIGN KEY (address) REFERENCES ${tables.heads} (address) ON DELETE CASCADE
|
|
41
|
+
)`,
|
|
42
|
+
`CREATE INDEX IF NOT EXISTS ${tables.events}_retention_idx ON ${tables.events} (namespace, committed_at_ms, address, sequence)`,
|
|
43
|
+
`CREATE TABLE IF NOT EXISTS ${tables.timers} (
|
|
44
|
+
address TEXT NOT NULL,
|
|
45
|
+
address_json TEXT NOT NULL,
|
|
46
|
+
namespace TEXT NOT NULL,
|
|
47
|
+
timer_id TEXT NOT NULL,
|
|
48
|
+
generation BIGINT NOT NULL CHECK (generation > 0),
|
|
49
|
+
status TEXT NOT NULL CHECK (status IN ('scheduled', 'cancelled', 'consumed')),
|
|
50
|
+
due_at_ms BIGINT,
|
|
51
|
+
command_name TEXT,
|
|
52
|
+
payload TEXT,
|
|
53
|
+
PRIMARY KEY (address, timer_id),
|
|
54
|
+
FOREIGN KEY (address) REFERENCES ${tables.heads} (address) ON DELETE CASCADE
|
|
55
|
+
)`,
|
|
56
|
+
`CREATE INDEX IF NOT EXISTS ${tables.timers}_due_idx ON ${tables.timers} (namespace, status, due_at_ms, address, timer_id)`,
|
|
57
|
+
`CREATE TABLE IF NOT EXISTS ${tables.outbox} (
|
|
58
|
+
delivery_id TEXT PRIMARY KEY,
|
|
59
|
+
source_address TEXT NOT NULL,
|
|
60
|
+
namespace TEXT NOT NULL,
|
|
61
|
+
source_command_id TEXT NOT NULL,
|
|
62
|
+
target_call TEXT NOT NULL,
|
|
63
|
+
status TEXT NOT NULL CHECK (status IN ('pending', 'claimed', 'settled')),
|
|
64
|
+
attempt INTEGER NOT NULL DEFAULT 0 CHECK (attempt >= 0),
|
|
65
|
+
claim_token TEXT,
|
|
66
|
+
claim_until_ms BIGINT,
|
|
67
|
+
retry_at_ms BIGINT NOT NULL DEFAULT 0,
|
|
68
|
+
outcome TEXT,
|
|
69
|
+
reason_code TEXT,
|
|
70
|
+
created_at_ms BIGINT NOT NULL,
|
|
71
|
+
FOREIGN KEY (source_address) REFERENCES ${tables.heads} (address) ON DELETE CASCADE
|
|
72
|
+
)`,
|
|
73
|
+
`CREATE INDEX IF NOT EXISTS ${tables.outbox}_claim_idx ON ${tables.outbox} (namespace, status, retry_at_ms, claim_until_ms, created_at_ms, delivery_id)`,
|
|
74
|
+
];
|
|
75
|
+
export const validateSchema = (sql, tables) => Effect.gen(function* () {
|
|
76
|
+
const versions = yield* sql.unsafe(`SELECT version FROM ${tables.schema} WHERE id = 1`);
|
|
77
|
+
if (versions.length !== 1)
|
|
78
|
+
throw new Error("Missing actor store schema version");
|
|
79
|
+
const version = integer(versions[0].version);
|
|
80
|
+
if (version !== schemaVersion) {
|
|
81
|
+
return yield* Effect.fail(new UnsupportedVersion({
|
|
82
|
+
actorType: "effect-actors/store",
|
|
83
|
+
version,
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
yield* sql.unsafe(`SELECT address, address_json, namespace, fence, lease_until_ms, schema_version, state, revision, event_sequence, event_floor FROM ${tables.heads} WHERE 1 = 0`);
|
|
87
|
+
yield* sql.unsafe(`SELECT address, command_id, fingerprint, outcome, revision, event_sequence, committed_at_ms FROM ${tables.receipts} WHERE 1 = 0`);
|
|
88
|
+
yield* sql.unsafe(`SELECT address, namespace, sequence, command_id, payload, committed_at_ms FROM ${tables.events} WHERE 1 = 0`);
|
|
89
|
+
yield* sql.unsafe(`SELECT address, address_json, namespace, timer_id, generation, status, due_at_ms, command_name, payload FROM ${tables.timers} WHERE 1 = 0`);
|
|
90
|
+
yield* sql.unsafe(`SELECT delivery_id, source_address, namespace, source_command_id, target_call, status, attempt, claim_token, claim_until_ms, retry_at_ms, outcome, reason_code, created_at_ms FROM ${tables.outbox} WHERE 1 = 0`);
|
|
91
|
+
})
|
|
92
|
+
.pipe(Effect.mapError(normalizeSqlError), Effect.catchDefect(() => Effect.fail(normalizeSqlError(undefined))))
|
|
93
|
+
.pipe(Effect.provideService(SqlClient.SafeIntegers, true));
|
|
94
|
+
export const migrate = (options) => protectSql(Effect.gen(function* () {
|
|
95
|
+
const tables = yield* protocolBoundary(() => validateTablePrefix(options.tablePrefix));
|
|
96
|
+
const sql = yield* SqlClient.SqlClient;
|
|
97
|
+
const dialect = yield* protocolBoundary(() => dialectOf(sql));
|
|
98
|
+
yield* sql.withTransaction(Effect.gen(function* () {
|
|
99
|
+
yield* sql.unsafe(`CREATE TABLE IF NOT EXISTS ${tables.schema} (
|
|
100
|
+
id INTEGER PRIMARY KEY CHECK (id = 1),
|
|
101
|
+
version INTEGER NOT NULL CHECK (version >= 0)
|
|
102
|
+
)`);
|
|
103
|
+
yield* sql.unsafe(`INSERT INTO ${tables.schema} (id, version) VALUES (1, 0) ON CONFLICT (id) DO NOTHING`);
|
|
104
|
+
const rows = yield* sql.unsafe(`SELECT version FROM ${tables.schema} WHERE id = 1${dialect === "pg" ? " FOR UPDATE" : ""}`);
|
|
105
|
+
if (rows.length !== 1)
|
|
106
|
+
throw new Error("Missing actor store schema version");
|
|
107
|
+
const current = integer(rows[0].version);
|
|
108
|
+
if (current > schemaVersion) {
|
|
109
|
+
return yield* Effect.fail(new UnsupportedVersion({
|
|
110
|
+
actorType: "effect-actors/store",
|
|
111
|
+
version: current,
|
|
112
|
+
}));
|
|
113
|
+
}
|
|
114
|
+
if (current < 0) {
|
|
115
|
+
return yield* Effect.fail(new InvalidInput({ message: "Invalid actor store schema version" }));
|
|
116
|
+
}
|
|
117
|
+
if (current === 0) {
|
|
118
|
+
for (const statement of migrationStatements(tables))
|
|
119
|
+
yield* sql.unsafe(statement);
|
|
120
|
+
yield* sql.unsafe(`UPDATE ${tables.schema} SET version = 1 WHERE id = 1 AND version = 0`);
|
|
121
|
+
}
|
|
122
|
+
yield* validateSchema(sql, tables);
|
|
123
|
+
}));
|
|
124
|
+
}));
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { Crypto, Effect } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
3
|
+
import { type StoreError } from "../../errors/index.js";
|
|
4
|
+
import type { Service, SqlOptions } from "../service.js";
|
|
5
|
+
export declare const make: (options: SqlOptions) => Effect.Effect<Service, StoreError, SqlClient.SqlClient | Crypto.Crypto>;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Crypto, Effect } from "effect";
|
|
2
|
+
import { SqlClient } from "effect/unstable/sql";
|
|
3
|
+
import { StorageUnavailable } from "../../errors/index.js";
|
|
4
|
+
import { collectEvents, events } from "./events.js";
|
|
5
|
+
import { dialectOf, normalizeSqlError, protocolBoundary, validateTablePrefix } from "./model.js";
|
|
6
|
+
import { acquire } from "./ownership.js";
|
|
7
|
+
import { claimOutbox, retryOutbox, settleOutbox } from "./outbox.js";
|
|
8
|
+
import { receipt } from "./receipts.js";
|
|
9
|
+
import { validateSchema } from "./schema.js";
|
|
10
|
+
import { initialize, query } from "./state.js";
|
|
11
|
+
import { dueTimers, fireTimer } from "./timers.js";
|
|
12
|
+
import { transact } from "./transaction.js";
|
|
13
|
+
export const make = (options) => Effect.gen(function* () {
|
|
14
|
+
const tables = yield* protocolBoundary(() => validateTablePrefix(options.tablePrefix));
|
|
15
|
+
const sql = yield* SqlClient.SqlClient;
|
|
16
|
+
const crypto = yield* Crypto.Crypto;
|
|
17
|
+
const dialect = yield* protocolBoundary(() => dialectOf(sql));
|
|
18
|
+
yield* validateSchema(sql, tables);
|
|
19
|
+
const context = {
|
|
20
|
+
sql,
|
|
21
|
+
crypto,
|
|
22
|
+
dialect,
|
|
23
|
+
tables,
|
|
24
|
+
owners: new WeakMap(),
|
|
25
|
+
};
|
|
26
|
+
const safeIntegers = (effect) => Effect.provideService(effect, SqlClient.SafeIntegers, true);
|
|
27
|
+
const service = {
|
|
28
|
+
acquire: (address, acquireOptions) => safeIntegers(acquire(context, address, acquireOptions)),
|
|
29
|
+
initialize: (owner, initializeOptions) => safeIntegers(initialize(context, owner, initializeOptions)),
|
|
30
|
+
transact: (owner, input, evaluate) => safeIntegers(transact(context, owner, input, evaluate)),
|
|
31
|
+
query: (owner, evaluate) => safeIntegers(query(context, owner, evaluate)),
|
|
32
|
+
receipt: (address, commandId) => safeIntegers(receipt(context, address, commandId)),
|
|
33
|
+
events: (address, eventOptions) => safeIntegers(events(context, address, eventOptions)),
|
|
34
|
+
claimOutbox: (claimOptions) => safeIntegers(claimOutbox(context, claimOptions)),
|
|
35
|
+
settleOutbox: (claim, outcome) => safeIntegers(settleOutbox(context, claim, outcome)),
|
|
36
|
+
retryOutbox: (claim, retryOptions) => safeIntegers(retryOutbox(context, claim, retryOptions)),
|
|
37
|
+
dueTimers: (timerOptions) => safeIntegers(dueTimers(context, timerOptions)),
|
|
38
|
+
fireTimer: (owner, candidate, evaluate) => safeIntegers(fireTimer(context, owner, candidate, evaluate)),
|
|
39
|
+
collectEvents: (retentionOptions) => safeIntegers(collectEvents(context, retentionOptions)),
|
|
40
|
+
};
|
|
41
|
+
return service;
|
|
42
|
+
}).pipe(Effect.mapError(normalizeSqlError), Effect.catchDefect(() => Effect.fail(new StorageUnavailable({ retryable: true }))));
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { Effect } from "effect";
|
|
2
|
+
import { type StoreError } from "../../errors/index.js";
|
|
3
|
+
import type { Json } from "../../protocol/index.js";
|
|
4
|
+
import type { Migration } from "../../actor/definition.js";
|
|
5
|
+
import type { Ownership, Snapshot } from "../service.js";
|
|
6
|
+
import { type HeadRow, type OwnerState, type SqlStoreContext } from "./ownership.js";
|
|
7
|
+
export declare const snapshotFromHead: (state: OwnerState, row: HeadRow) => Effect.Effect<Snapshot, StoreError>;
|
|
8
|
+
export declare const initialize: (context: SqlStoreContext, owner: Ownership, options: {
|
|
9
|
+
readonly currentVersion: number;
|
|
10
|
+
readonly initial: Json;
|
|
11
|
+
readonly migrations: ReadonlyArray<Migration>;
|
|
12
|
+
}) => Effect.Effect<Snapshot, StoreError>;
|
|
13
|
+
export declare const query: <A>(context: SqlStoreContext, owner: Ownership, evaluate: (snapshot: Snapshot) => A) => Effect.Effect<A, StoreError>;
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { Effect, Result } from "effect";
|
|
2
|
+
import { MigrationRejected, OwnershipLost, StorageUnavailable, UnsupportedVersion, } from "../../errors/index.js";
|
|
3
|
+
import { makeCursor } from "../../protocol/index.js";
|
|
4
|
+
import { databaseNow, decimal, decodeJson, encodeJson, integer, normalizeCallbackError, normalizeSqlError, positiveInteger, protocolBoundary, } from "./model.js";
|
|
5
|
+
import { finalFence, ownedHead, withOwner, } from "./ownership.js";
|
|
6
|
+
const invalidStoredData = () => new StorageUnavailable({ retryable: false });
|
|
7
|
+
export const snapshotFromHead = (state, row) => row.schema_version === null ||
|
|
8
|
+
row.schema_version === undefined ||
|
|
9
|
+
row.state === null ||
|
|
10
|
+
row.state === undefined
|
|
11
|
+
? Effect.fail(new UnsupportedVersion({ actorType: state.address.actorType, version: 0 }))
|
|
12
|
+
: protocolBoundary(() => ({
|
|
13
|
+
schemaVersion: integer(row.schema_version),
|
|
14
|
+
state: decodeJson(row.state),
|
|
15
|
+
revision: decimal(row.revision),
|
|
16
|
+
cursor: makeCursor(state.address, decimal(row.event_sequence)),
|
|
17
|
+
})).pipe(Effect.mapError(() => invalidStoredData()));
|
|
18
|
+
export const initialize = (context, owner, options) => withOwner(context, owner, (state) => {
|
|
19
|
+
const program = Effect.gen(function* () {
|
|
20
|
+
const currentVersion = yield* protocolBoundary(() => positiveInteger(options.currentVersion, "currentVersion"));
|
|
21
|
+
const initial = yield* protocolBoundary(() => encodeJson(options.initial));
|
|
22
|
+
const head = yield* ownedHead(context, state, true);
|
|
23
|
+
if (head.schema_version === null || head.schema_version === undefined) {
|
|
24
|
+
const updated = yield* context.sql `
|
|
25
|
+
UPDATE ${context.sql(context.tables.heads)}
|
|
26
|
+
SET schema_version = ${currentVersion}, state = ${initial}
|
|
27
|
+
WHERE address = ${state.addressId} AND fence = ${state.fence} AND schema_version IS NULL
|
|
28
|
+
AND lease_until_ms > ${databaseNow(context.sql, context.dialect)}
|
|
29
|
+
RETURNING schema_version
|
|
30
|
+
`;
|
|
31
|
+
if (updated.length !== 1)
|
|
32
|
+
return yield* Effect.fail(new OwnershipLost({ actorType: state.address.actorType }));
|
|
33
|
+
yield* finalFence(context, state);
|
|
34
|
+
return {
|
|
35
|
+
schemaVersion: currentVersion,
|
|
36
|
+
state: options.initial,
|
|
37
|
+
revision: decimal(head.revision),
|
|
38
|
+
cursor: yield* protocolBoundary(() => makeCursor(state.address, decimal(head.event_sequence))),
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
const storedVersion = integer(head.schema_version);
|
|
42
|
+
if (storedVersion > currentVersion) {
|
|
43
|
+
return yield* Effect.fail(new UnsupportedVersion({ actorType: state.address.actorType, version: storedVersion }));
|
|
44
|
+
}
|
|
45
|
+
if (storedVersion === currentVersion)
|
|
46
|
+
return yield* snapshotFromHead(state, head);
|
|
47
|
+
let version = storedVersion;
|
|
48
|
+
let encoded = decodeJson(head.state);
|
|
49
|
+
while (version < currentVersion) {
|
|
50
|
+
const candidates = options.migrations.filter((migration) => migration.fromVersion === version);
|
|
51
|
+
if (candidates.length !== 1) {
|
|
52
|
+
return yield* Effect.fail(new MigrationRejected({
|
|
53
|
+
fromVersion: version,
|
|
54
|
+
toVersion: currentVersion,
|
|
55
|
+
message: "Migration chain is incomplete or ambiguous",
|
|
56
|
+
}));
|
|
57
|
+
}
|
|
58
|
+
const migration = candidates[0];
|
|
59
|
+
if (!Number.isSafeInteger(migration.toVersion) ||
|
|
60
|
+
migration.toVersion <= version ||
|
|
61
|
+
migration.toVersion > currentVersion) {
|
|
62
|
+
return yield* Effect.fail(new MigrationRejected({
|
|
63
|
+
fromVersion: migration.fromVersion,
|
|
64
|
+
toVersion: migration.toVersion,
|
|
65
|
+
message: "Migration chain does not advance toward the current version",
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
const result = yield* Effect.try({
|
|
69
|
+
try: () => migration.migrate(encoded),
|
|
70
|
+
catch: normalizeCallbackError,
|
|
71
|
+
});
|
|
72
|
+
if (Result.isFailure(result))
|
|
73
|
+
return yield* Effect.fail(result.failure);
|
|
74
|
+
encoded = result.success;
|
|
75
|
+
yield* protocolBoundary(() => encodeJson(encoded));
|
|
76
|
+
version = migration.toVersion;
|
|
77
|
+
}
|
|
78
|
+
const stored = yield* protocolBoundary(() => encodeJson(encoded));
|
|
79
|
+
const updated = yield* context.sql `
|
|
80
|
+
UPDATE ${context.sql(context.tables.heads)}
|
|
81
|
+
SET schema_version = ${currentVersion}, state = ${stored}
|
|
82
|
+
WHERE address = ${state.addressId} AND fence = ${state.fence} AND schema_version = ${storedVersion}
|
|
83
|
+
AND lease_until_ms > ${databaseNow(context.sql, context.dialect)}
|
|
84
|
+
RETURNING schema_version
|
|
85
|
+
`;
|
|
86
|
+
if (updated.length !== 1)
|
|
87
|
+
return yield* Effect.fail(new OwnershipLost({ actorType: state.address.actorType }));
|
|
88
|
+
yield* finalFence(context, state);
|
|
89
|
+
return {
|
|
90
|
+
schemaVersion: currentVersion,
|
|
91
|
+
state: encoded,
|
|
92
|
+
revision: decimal(head.revision),
|
|
93
|
+
cursor: yield* protocolBoundary(() => makeCursor(state.address, decimal(head.event_sequence))),
|
|
94
|
+
};
|
|
95
|
+
});
|
|
96
|
+
return context.sql.withTransaction(program).pipe(Effect.mapError(normalizeSqlError), Effect.catchDefect(() => Effect.fail(new StorageUnavailable({ retryable: true }))));
|
|
97
|
+
});
|
|
98
|
+
export const query = (context, owner, evaluate) => withOwner(context, owner, (state) => {
|
|
99
|
+
const program = Effect.gen(function* () {
|
|
100
|
+
const snapshot = yield* ownedHead(context, state, true).pipe(Effect.flatMap((head) => snapshotFromHead(state, head)));
|
|
101
|
+
const value = yield* Effect.try({
|
|
102
|
+
try: () => evaluate(snapshot),
|
|
103
|
+
catch: normalizeCallbackError,
|
|
104
|
+
});
|
|
105
|
+
yield* ownedHead(context, state, false);
|
|
106
|
+
return value;
|
|
107
|
+
});
|
|
108
|
+
return context.sql.withTransaction(program).pipe(Effect.mapError(normalizeSqlError), Effect.catchDefect(() => Effect.fail(new StorageUnavailable({ retryable: true }))));
|
|
109
|
+
});
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { Effect, Option, Result } from "effect";
|
|
2
|
+
import { type StoreError } from "../../errors/index.js";
|
|
3
|
+
import type { Json, WireReceipt } from "../../protocol/index.js";
|
|
4
|
+
import type { EncodedTransition, Ownership, TimerCandidate, Turn } from "../service.js";
|
|
5
|
+
import { type SqlStoreContext } from "./ownership.js";
|
|
6
|
+
export declare const dueTimers: (context: SqlStoreContext, options: {
|
|
7
|
+
readonly namespace: string;
|
|
8
|
+
readonly limit: number;
|
|
9
|
+
}) => Effect.Effect<ReadonlyArray<TimerCandidate>, StoreError>;
|
|
10
|
+
export declare const fireTimer: (context: SqlStoreContext, owner: Ownership, candidate: TimerCandidate, evaluate: (command: string, payload: Json, turn: Turn) => Result.Result<EncodedTransition, Json>) => Effect.Effect<Option.Option<WireReceipt>, StoreError>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { Effect, Option, Result } from "effect";
|
|
2
|
+
import { InvalidInput, StorageUnavailable } from "../../errors/index.js";
|
|
3
|
+
import { addressId, validateId } from "../../protocol/index.js";
|
|
4
|
+
import { databaseNow, decimal, decodeAddress, decodeJson, integer, normalizeCallbackError, normalizeSqlError, positiveInteger, protocolBoundary, } from "./model.js";
|
|
5
|
+
import { ownedHead, withOwner } from "./ownership.js";
|
|
6
|
+
import { snapshotFromHead } from "./state.js";
|
|
7
|
+
import { applyEvaluation, timerCommandIdentity, timerFingerprint } from "./transition.js";
|
|
8
|
+
export const dueTimers = (context, options) => Effect.gen(function* () {
|
|
9
|
+
const limit = yield* protocolBoundary(() => positiveInteger(options.limit, "timer scan limit"));
|
|
10
|
+
if (typeof options.namespace !== "string" || options.namespace.length === 0) {
|
|
11
|
+
return yield* Effect.fail(new InvalidInput({ message: "Invalid timer namespace" }));
|
|
12
|
+
}
|
|
13
|
+
const { dialect, sql, tables } = context;
|
|
14
|
+
const rows = yield* sql `
|
|
15
|
+
SELECT address, address_json, timer_id, generation, due_at_ms
|
|
16
|
+
FROM ${sql(tables.timers)}
|
|
17
|
+
WHERE namespace = ${options.namespace}
|
|
18
|
+
AND status = 'scheduled'
|
|
19
|
+
AND due_at_ms <= ${databaseNow(sql, dialect)}
|
|
20
|
+
ORDER BY due_at_ms, address, timer_id
|
|
21
|
+
LIMIT ${limit}
|
|
22
|
+
`;
|
|
23
|
+
return yield* protocolBoundary(() => rows.map((row) => {
|
|
24
|
+
if (typeof row.address !== "string" || typeof row.timer_id !== "string")
|
|
25
|
+
throw new Error("Invalid timer row");
|
|
26
|
+
return {
|
|
27
|
+
address: decodeAddress(row.address_json, row.address),
|
|
28
|
+
timerId: row.timer_id,
|
|
29
|
+
generation: decimal(row.generation),
|
|
30
|
+
dueAtMillis: integer(row.due_at_ms),
|
|
31
|
+
};
|
|
32
|
+
})).pipe(Effect.mapError(() => new StorageUnavailable({ retryable: false })));
|
|
33
|
+
}).pipe(Effect.mapError(normalizeSqlError), Effect.catchDefect(() => Effect.fail(new StorageUnavailable({ retryable: true }))));
|
|
34
|
+
export const fireTimer = (context, owner, candidate, evaluate) => withOwner(context, owner, (state) => {
|
|
35
|
+
const program = Effect.gen(function* () {
|
|
36
|
+
const candidateAddress = yield* protocolBoundary(() => addressId(candidate.address));
|
|
37
|
+
if (candidateAddress !== state.addressId) {
|
|
38
|
+
return yield* Effect.fail(new InvalidInput({ message: "Timer candidate does not match its owner" }));
|
|
39
|
+
}
|
|
40
|
+
yield* protocolBoundary(() => {
|
|
41
|
+
validateId(candidate.timerId);
|
|
42
|
+
decimal(candidate.generation);
|
|
43
|
+
if (!Number.isSafeInteger(candidate.dueAtMillis) || candidate.dueAtMillis < 0) {
|
|
44
|
+
throw new InvalidInput({ message: "Invalid timer candidate" });
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
const head = yield* ownedHead(context, state, true);
|
|
48
|
+
const snapshot = yield* snapshotFromHead(state, head);
|
|
49
|
+
const rows = yield* context.sql `
|
|
50
|
+
UPDATE ${context.sql(context.tables.timers)}
|
|
51
|
+
SET status = 'consumed'
|
|
52
|
+
WHERE address = ${state.addressId}
|
|
53
|
+
AND timer_id = ${candidate.timerId}
|
|
54
|
+
AND generation = ${candidate.generation}
|
|
55
|
+
AND due_at_ms = ${candidate.dueAtMillis}
|
|
56
|
+
AND status = 'scheduled'
|
|
57
|
+
AND due_at_ms <= ${databaseNow(context.sql, context.dialect)}
|
|
58
|
+
RETURNING command_name, payload
|
|
59
|
+
`;
|
|
60
|
+
if (rows.length === 0)
|
|
61
|
+
return Option.none();
|
|
62
|
+
const row = rows[0];
|
|
63
|
+
const decoded = yield* protocolBoundary(() => {
|
|
64
|
+
if (typeof row.command_name !== "string" ||
|
|
65
|
+
row.payload === null ||
|
|
66
|
+
row.payload === undefined) {
|
|
67
|
+
throw new Error("Invalid timer intention");
|
|
68
|
+
}
|
|
69
|
+
return { command: row.command_name, payload: decodeJson(row.payload) };
|
|
70
|
+
}).pipe(Effect.mapError(() => new StorageUnavailable({ retryable: false })));
|
|
71
|
+
const evaluation = yield* Effect.try({
|
|
72
|
+
try: () => evaluate(decoded.command, decoded.payload, {
|
|
73
|
+
snapshot,
|
|
74
|
+
nowMillis: integer(head.now_ms),
|
|
75
|
+
}),
|
|
76
|
+
catch: normalizeCallbackError,
|
|
77
|
+
});
|
|
78
|
+
const commandId = yield* protocolBoundary(() => timerCommandIdentity(candidate.timerId, candidate.generation));
|
|
79
|
+
const fingerprint = yield* protocolBoundary(() => timerFingerprint(candidate.timerId, candidate.generation, decoded.command, decoded.payload));
|
|
80
|
+
return Option.some(yield* applyEvaluation(context, state, {
|
|
81
|
+
commandId,
|
|
82
|
+
fingerprint,
|
|
83
|
+
snapshot,
|
|
84
|
+
schemaVersion: snapshot.schemaVersion,
|
|
85
|
+
eventSequence: decimal(head.event_sequence),
|
|
86
|
+
evaluation,
|
|
87
|
+
}));
|
|
88
|
+
});
|
|
89
|
+
return context.sql.withTransaction(program).pipe(Effect.mapError(normalizeSqlError), Effect.catchDefect(() => Effect.fail(new StorageUnavailable({ retryable: true }))));
|
|
90
|
+
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Effect, Result } from "effect";
|
|
2
|
+
import { CommandConflict, type StoreError } from "../../errors/index.js";
|
|
3
|
+
import type { Invocation, Json, WireReceipt } from "../../protocol/index.js";
|
|
4
|
+
import type { EncodedTransition, Ownership, Turn } from "../service.js";
|
|
5
|
+
import { type SqlStoreContext } from "./ownership.js";
|
|
6
|
+
export declare const transact: (context: SqlStoreContext, owner: Ownership, input: Invocation, evaluate: (turn: Turn) => Result.Result<EncodedTransition, Json>) => Effect.Effect<WireReceipt, StoreError | CommandConflict>;
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Effect, Result } from "effect";
|
|
2
|
+
import { CommandConflict, InvalidInput, StorageUnavailable, } from "../../errors/index.js";
|
|
3
|
+
import { addressId, validateId } from "../../protocol/index.js";
|
|
4
|
+
import { decimal, decimalInput, integer, normalizeCallbackError, normalizeSqlError, protocolBoundary, } from "./model.js";
|
|
5
|
+
import { ownedHead, withOwner } from "./ownership.js";
|
|
6
|
+
import { recordFailure, storedReceipt } from "./receipts.js";
|
|
7
|
+
import { snapshotFromHead } from "./state.js";
|
|
8
|
+
import { applyEvaluation } from "./transition.js";
|
|
9
|
+
export const transact = (context, owner, input, evaluate) => withOwner(context, owner, (state) => {
|
|
10
|
+
const program = Effect.gen(function* () {
|
|
11
|
+
const inputAddress = yield* protocolBoundary(() => addressId(input.address));
|
|
12
|
+
if (inputAddress !== state.addressId) {
|
|
13
|
+
return yield* Effect.fail(new InvalidInput({ message: "Invocation does not match its owner" }));
|
|
14
|
+
}
|
|
15
|
+
yield* protocolBoundary(() => validateId(input.commandId));
|
|
16
|
+
if (typeof input.fingerprint !== "string")
|
|
17
|
+
return yield* Effect.fail(new InvalidInput({ message: "Invalid command identity" }));
|
|
18
|
+
const head = yield* ownedHead(context, state, true);
|
|
19
|
+
const snapshot = yield* snapshotFromHead(state, head);
|
|
20
|
+
const duplicate = yield* storedReceipt(context, state, input.commandId);
|
|
21
|
+
if (duplicate !== undefined) {
|
|
22
|
+
if (duplicate.fingerprint !== input.fingerprint) {
|
|
23
|
+
return yield* Effect.fail(new CommandConflict({ commandId: input.commandId }));
|
|
24
|
+
}
|
|
25
|
+
return duplicate.receipt;
|
|
26
|
+
}
|
|
27
|
+
if (input.expectedRevision !== undefined) {
|
|
28
|
+
yield* protocolBoundary(() => decimalInput(input.expectedRevision, "expectedRevision"));
|
|
29
|
+
if (input.expectedRevision !== snapshot.revision) {
|
|
30
|
+
return yield* recordFailure(context, state, {
|
|
31
|
+
commandId: input.commandId,
|
|
32
|
+
fingerprint: input.fingerprint,
|
|
33
|
+
error: {
|
|
34
|
+
_tag: "effect-actors/RevisionConflict",
|
|
35
|
+
expected: input.expectedRevision,
|
|
36
|
+
actual: snapshot.revision,
|
|
37
|
+
},
|
|
38
|
+
revision: snapshot.revision,
|
|
39
|
+
eventSequence: decimal(head.event_sequence),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
const evaluation = yield* Effect.try({
|
|
44
|
+
try: () => evaluate({ snapshot, nowMillis: integer(head.now_ms) }),
|
|
45
|
+
catch: normalizeCallbackError,
|
|
46
|
+
});
|
|
47
|
+
return yield* applyEvaluation(context, state, {
|
|
48
|
+
commandId: input.commandId,
|
|
49
|
+
fingerprint: input.fingerprint,
|
|
50
|
+
snapshot,
|
|
51
|
+
schemaVersion: snapshot.schemaVersion,
|
|
52
|
+
eventSequence: decimal(head.event_sequence),
|
|
53
|
+
evaluation,
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
return context.sql.withTransaction(program).pipe(Effect.mapError(normalizeSqlError), Effect.catchDefect(() => Effect.fail(new StorageUnavailable({ retryable: true }))));
|
|
57
|
+
});
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Effect, Result } from "effect";
|
|
2
|
+
import { type StoreError } from "../../errors/index.js";
|
|
3
|
+
import type { Json, WireReceipt } from "../../protocol/index.js";
|
|
4
|
+
import type { EncodedTransition, Snapshot } from "../service.js";
|
|
5
|
+
import { type OwnerState, type SqlStoreContext } from "./ownership.js";
|
|
6
|
+
export declare const applyEvaluation: (context: SqlStoreContext, state: OwnerState, input: {
|
|
7
|
+
readonly commandId: string;
|
|
8
|
+
readonly fingerprint: string;
|
|
9
|
+
readonly snapshot: Snapshot;
|
|
10
|
+
readonly schemaVersion: number;
|
|
11
|
+
readonly eventSequence: string;
|
|
12
|
+
readonly evaluation: Result.Result<EncodedTransition, Json>;
|
|
13
|
+
}) => Effect.Effect<WireReceipt, StoreError>;
|
|
14
|
+
export declare const timerCommandIdentity: (timerId: string, generation: string) => string;
|
|
15
|
+
export declare const timerFingerprint: (timerId: string, generation: string, command: string, payload: Json) => string;
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
import { Effect, Result } from "effect";
|
|
2
|
+
import { InvalidInput, OwnershipLost, StorageUnavailable, } from "../../errors/index.js";
|
|
3
|
+
import { addressId, canonicalJson, validateId } from "../../protocol/index.js";
|
|
4
|
+
import { databaseNow, encodeJson, nonNegativeInteger, normalizeSqlError, protocolBoundary, } from "./model.js";
|
|
5
|
+
import { finalFence } from "./ownership.js";
|
|
6
|
+
import { committedAt, insertReceipt, recordFailure } from "./receipts.js";
|
|
7
|
+
const prepareTransition = (source, transition) => protocolBoundary(() => {
|
|
8
|
+
const state = encodeJson(transition.state);
|
|
9
|
+
encodeJson(transition.value);
|
|
10
|
+
const events = transition.events.map((value) => ({ encoded: encodeJson(value) }));
|
|
11
|
+
const timers = transition.timers.map((change) => {
|
|
12
|
+
validateId(change.timerId);
|
|
13
|
+
if (change._tag === "Cancel")
|
|
14
|
+
return { _tag: "Cancel", timerId: change.timerId };
|
|
15
|
+
nonNegativeInteger(change.dueAtMillis, "timer dueAtMillis");
|
|
16
|
+
if (typeof change.command !== "string" ||
|
|
17
|
+
change.command.length === 0 ||
|
|
18
|
+
change.command.length > 256) {
|
|
19
|
+
throw new InvalidInput({ message: "Invalid timer command" });
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
_tag: "Set",
|
|
23
|
+
timerId: change.timerId,
|
|
24
|
+
dueAtMillis: change.dueAtMillis,
|
|
25
|
+
command: change.command,
|
|
26
|
+
payload: encodeJson(change.payload),
|
|
27
|
+
};
|
|
28
|
+
});
|
|
29
|
+
const outbox = transition.outbox.map((call) => {
|
|
30
|
+
if (call.target.namespace !== source.namespace || call.target.tenant !== source.tenant) {
|
|
31
|
+
throw new InvalidInput({
|
|
32
|
+
message: "Outbox targets must remain in the source namespace and tenant",
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
addressId(call.target);
|
|
36
|
+
if ((call.kind !== "command" && call.kind !== "hosted") ||
|
|
37
|
+
typeof call.operation !== "string" ||
|
|
38
|
+
call.operation.length === 0 ||
|
|
39
|
+
!Number.isSafeInteger(call.protocolVersion) ||
|
|
40
|
+
call.protocolVersion < 0 ||
|
|
41
|
+
typeof call.commandId !== "string") {
|
|
42
|
+
throw new InvalidInput({ message: "Invalid outgoing command" });
|
|
43
|
+
}
|
|
44
|
+
validateId(call.commandId);
|
|
45
|
+
encodeJson(call.payload);
|
|
46
|
+
return {
|
|
47
|
+
targetCall: encodeJson(call),
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
return { state, value: transition.value, events, timers, outbox };
|
|
51
|
+
});
|
|
52
|
+
const applyTimers = (context, state, timers) => Effect.gen(function* () {
|
|
53
|
+
const { sql, tables } = context;
|
|
54
|
+
const addressJson = encodeJson(state.address);
|
|
55
|
+
for (const timer of timers) {
|
|
56
|
+
const updated = timer._tag === "Set"
|
|
57
|
+
? yield* sql `
|
|
58
|
+
UPDATE ${sql(tables.timers)}
|
|
59
|
+
SET generation = generation + 1, status = 'scheduled', due_at_ms = ${timer.dueAtMillis},
|
|
60
|
+
command_name = ${timer.command}, payload = ${timer.payload}, address_json = ${addressJson},
|
|
61
|
+
namespace = ${state.address.namespace}
|
|
62
|
+
WHERE address = ${state.addressId} AND timer_id = ${timer.timerId}
|
|
63
|
+
RETURNING generation
|
|
64
|
+
`
|
|
65
|
+
: yield* sql `
|
|
66
|
+
UPDATE ${sql(tables.timers)}
|
|
67
|
+
SET generation = generation + 1, status = 'cancelled', due_at_ms = NULL,
|
|
68
|
+
command_name = NULL, payload = NULL, address_json = ${addressJson},
|
|
69
|
+
namespace = ${state.address.namespace}
|
|
70
|
+
WHERE address = ${state.addressId} AND timer_id = ${timer.timerId}
|
|
71
|
+
RETURNING generation
|
|
72
|
+
`;
|
|
73
|
+
if (updated.length > 0)
|
|
74
|
+
continue;
|
|
75
|
+
if (timer._tag === "Set") {
|
|
76
|
+
yield* sql `
|
|
77
|
+
INSERT INTO ${sql(tables.timers)}
|
|
78
|
+
(address, address_json, namespace, timer_id, generation, status, due_at_ms, command_name, payload)
|
|
79
|
+
VALUES (${state.addressId}, ${addressJson}, ${state.address.namespace}, ${timer.timerId}, 1,
|
|
80
|
+
'scheduled', ${timer.dueAtMillis}, ${timer.command}, ${timer.payload})
|
|
81
|
+
`;
|
|
82
|
+
}
|
|
83
|
+
else {
|
|
84
|
+
yield* sql `
|
|
85
|
+
INSERT INTO ${sql(tables.timers)}
|
|
86
|
+
(address, address_json, namespace, timer_id, generation, status)
|
|
87
|
+
VALUES (${state.addressId}, ${addressJson}, ${state.address.namespace}, ${timer.timerId}, 1, 'cancelled')
|
|
88
|
+
`;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
}).pipe(Effect.mapError(normalizeSqlError));
|
|
92
|
+
const applyOutbox = (context, state, sourceCommandId, calls, nowMillis) => Effect.gen(function* () {
|
|
93
|
+
const { sql, tables } = context;
|
|
94
|
+
for (const call of calls) {
|
|
95
|
+
const deliveryId = yield* context.crypto.randomUUIDv4.pipe(Effect.mapError(() => new StorageUnavailable({ retryable: true })));
|
|
96
|
+
yield* sql `
|
|
97
|
+
INSERT INTO ${sql(tables.outbox)}
|
|
98
|
+
(delivery_id, source_address, namespace, source_command_id, target_call, status,
|
|
99
|
+
attempt, retry_at_ms, created_at_ms)
|
|
100
|
+
VALUES (${deliveryId}, ${state.addressId}, ${state.address.namespace}, ${sourceCommandId},
|
|
101
|
+
${call.targetCall}, 'pending', 0, 0, ${nowMillis})
|
|
102
|
+
`;
|
|
103
|
+
}
|
|
104
|
+
}).pipe(Effect.mapError(normalizeSqlError));
|
|
105
|
+
export const applyEvaluation = (context, state, input) => Effect.gen(function* () {
|
|
106
|
+
const currentRevision = input.snapshot.revision;
|
|
107
|
+
const sequenceBefore = input.eventSequence;
|
|
108
|
+
if (Result.isFailure(input.evaluation)) {
|
|
109
|
+
return yield* recordFailure(context, state, {
|
|
110
|
+
commandId: input.commandId,
|
|
111
|
+
fingerprint: input.fingerprint,
|
|
112
|
+
error: input.evaluation.failure,
|
|
113
|
+
revision: currentRevision,
|
|
114
|
+
eventSequence: sequenceBefore,
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
const prepared = yield* prepareTransition(state.address, input.evaluation.success);
|
|
118
|
+
const nextRevision = (BigInt(currentRevision) + 1n).toString();
|
|
119
|
+
const nextSequence = (BigInt(sequenceBefore) + BigInt(prepared.events.length)).toString();
|
|
120
|
+
const nowMillis = yield* committedAt(context);
|
|
121
|
+
const updated = yield* context.sql `
|
|
122
|
+
UPDATE ${context.sql(context.tables.heads)}
|
|
123
|
+
SET state = ${prepared.state}, schema_version = ${input.schemaVersion}, revision = ${nextRevision},
|
|
124
|
+
event_sequence = ${nextSequence}
|
|
125
|
+
WHERE address = ${state.addressId} AND fence = ${state.fence} AND revision = ${currentRevision}
|
|
126
|
+
AND lease_until_ms > ${databaseNow(context.sql, context.dialect)}
|
|
127
|
+
RETURNING revision, event_sequence
|
|
128
|
+
`;
|
|
129
|
+
if (updated.length !== 1)
|
|
130
|
+
return yield* Effect.fail(new OwnershipLost({ actorType: state.address.actorType }));
|
|
131
|
+
for (let index = 0; index < prepared.events.length; index++) {
|
|
132
|
+
const sequence = (BigInt(sequenceBefore) + BigInt(index) + 1n).toString();
|
|
133
|
+
yield* context.sql `
|
|
134
|
+
INSERT INTO ${context.sql(context.tables.events)}
|
|
135
|
+
(address, namespace, sequence, command_id, payload, committed_at_ms)
|
|
136
|
+
VALUES (${state.addressId}, ${state.address.namespace}, ${sequence}, ${input.commandId},
|
|
137
|
+
${prepared.events[index].encoded}, ${nowMillis})
|
|
138
|
+
`;
|
|
139
|
+
}
|
|
140
|
+
yield* applyTimers(context, state, prepared.timers);
|
|
141
|
+
yield* applyOutbox(context, state, input.commandId, prepared.outbox, nowMillis);
|
|
142
|
+
const receipt = yield* insertReceipt(context, state, {
|
|
143
|
+
commandId: input.commandId,
|
|
144
|
+
fingerprint: input.fingerprint,
|
|
145
|
+
outcome: { _tag: "Success", value: prepared.value },
|
|
146
|
+
revision: nextRevision,
|
|
147
|
+
eventSequence: nextSequence,
|
|
148
|
+
committedAtMillis: nowMillis,
|
|
149
|
+
});
|
|
150
|
+
yield* finalFence(context, state);
|
|
151
|
+
return receipt;
|
|
152
|
+
}).pipe(Effect.mapError(normalizeSqlError));
|
|
153
|
+
export const timerCommandIdentity = (timerId, generation) => `~effect-actors/timer/${canonicalJson([timerId, generation])}`;
|
|
154
|
+
export const timerFingerprint = (timerId, generation, command, payload) => canonicalJson({ kind: "effect-actors/timer", timerId, generation, command, payload });
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { Layer } from "effect";
|
|
2
|
+
import type { Client } from "../client/service.js";
|
|
3
|
+
import { type Configuration, type Runtime } from "../runtime/service.js";
|
|
4
|
+
import { type Store } from "../store/service.js";
|
|
5
|
+
export declare const configuration: Configuration;
|
|
6
|
+
export declare const layerMemory: Layer.Layer<Runtime | Client | Store>;
|