@rikalabs/akter 0.1.0-alpha.4 → 0.1.0-alpha.6

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.
Files changed (63) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/dist/actor/descriptor.js +3 -0
  3. package/dist/actor/handles.d.ts +1 -1
  4. package/dist/actor/layers.js +2 -0
  5. package/dist/actor/reads.js +2 -1
  6. package/dist/contexts/command.d.ts +2 -0
  7. package/dist/identity/caller.d.ts +4 -4
  8. package/dist/identity/caller.js +11 -2
  9. package/dist/identity/mint.d.ts +1 -1
  10. package/dist/policies/command.d.ts +3 -0
  11. package/dist/policies/command.js +1 -0
  12. package/dist/runtime/connections/owner.js +60 -13
  13. package/dist/runtime/connections/protocol.d.ts +2 -2
  14. package/dist/runtime/connections/streams.js +2 -1
  15. package/dist/runtime/database/bounded.js +17 -2
  16. package/dist/runtime/database/connection.d.ts +3 -2
  17. package/dist/runtime/database/connection.js +9 -4
  18. package/dist/runtime/database/coordination.d.ts +6 -3
  19. package/dist/runtime/database/coordination.js +19 -18
  20. package/dist/runtime/database/migrations.d.ts +4 -0
  21. package/dist/runtime/database/migrations.js +72 -1
  22. package/dist/runtime/entity/mailbox.d.ts +18 -10
  23. package/dist/runtime/entity/mailbox.js +42 -34
  24. package/dist/runtime/entity/register.d.ts +3 -3
  25. package/dist/runtime/entity/register.js +280 -271
  26. package/dist/runtime/events/append.d.ts +3 -3
  27. package/dist/runtime/index.d.ts +4 -1
  28. package/dist/runtime/index.js +2 -1
  29. package/dist/runtime/layer.d.ts +24 -1
  30. package/dist/runtime/layer.js +64 -9
  31. package/dist/runtime/members.d.ts +2 -0
  32. package/dist/runtime/operators/export.d.ts +3 -1
  33. package/dist/runtime/operators/export.js +42 -11
  34. package/dist/runtime/queries.d.ts +1 -1
  35. package/dist/runtime/queries.js +23 -6
  36. package/dist/runtime/registration.js +27 -1
  37. package/dist/runtime/request.d.ts +1 -1
  38. package/dist/runtime/runner.d.ts +18 -1
  39. package/dist/runtime/runner.js +24 -4
  40. package/dist/runtime/storage/cold-storage.d.ts +56 -0
  41. package/dist/runtime/storage/cold-storage.js +138 -0
  42. package/dist/runtime/storage/cold-tier.d.ts +141 -0
  43. package/dist/runtime/storage/cold-tier.js +355 -0
  44. package/dist/runtime/storage/generation.d.ts +14 -0
  45. package/dist/runtime/storage/generation.js +2 -0
  46. package/dist/runtime/storage/placements.js +1 -0
  47. package/dist/runtime/telemetry/metrics.d.ts +4 -0
  48. package/dist/runtime/telemetry/metrics.js +20 -4
  49. package/dist/runtime/telemetry/sampler.js +4 -3
  50. package/dist/runtime/telemetry/spans.d.ts +1 -1
  51. package/dist/runtime/topology/locks.js +8 -6
  52. package/dist/runtime/turn/blobs.d.ts +4 -1
  53. package/dist/runtime/turn/blobs.js +19 -2
  54. package/dist/runtime/turn/execute.d.ts +3 -1
  55. package/dist/runtime/turn/execute.js +272 -20
  56. package/dist/runtime/turn/hooks.d.ts +1 -1
  57. package/dist/runtime/turn/outbox.d.ts +1 -1
  58. package/dist/runtime/turn/receipt.d.ts +1 -1
  59. package/dist/runtime/turn/relay.d.ts +2 -2
  60. package/dist/runtime/turn/relay.js +53 -7
  61. package/dist/testing/actor-test.d.ts +1 -0
  62. package/dist/testing/actor-test.js +4 -1
  63. package/package.json +1 -1
@@ -1213,6 +1213,77 @@ export const migrations = {
1213
1213
  ${[...FIRST_VIEWS, ["views", 1]].map(([view, version]) => `('${view}', ${version})`).join(", ")}
1214
1214
  ) AS v(view_name, version)`);
1215
1215
  }),
1216
+ /** Cold pointers replace state and blob rows atomically; garbage rows never authorize deletion by themselves. */
1217
+ "0034_cold_tier": Effect.gen(function* () {
1218
+ const sql = yield* SqlClient.SqlClient;
1219
+ yield* briefLocks(Effect.gen(function* () {
1220
+ yield* sql `ALTER TABLE actor_generations
1221
+ ADD COLUMN cold_ref text,
1222
+ ADD COLUMN cold_digest text,
1223
+ ADD COLUMN cold_state_version integer,
1224
+ ADD CONSTRAINT actor_generations_cold_check CHECK (
1225
+ (cold_ref IS NULL AND cold_digest IS NULL AND cold_state_version IS NULL)
1226
+ OR (cold_ref IS NOT NULL AND cold_digest IS NOT NULL AND cold_state_version IS NOT NULL
1227
+ AND cold_digest ~ '^[0-9a-f]{64}$' AND cold_state_version >= 0))`;
1228
+ yield* sql `ALTER TABLE actor_outbox DROP CONSTRAINT actor_outbox_kind_check,
1229
+ ADD CONSTRAINT actor_outbox_kind_check CHECK (kind IN ('intent', 'job', 'feed', 'control', 'cold')),
1230
+ ADD CONSTRAINT actor_outbox_cold_check CHECK (
1231
+ (kind = 'cold') = (command = '$cold') AND
1232
+ (kind <> 'cold' OR (timer_key = '$cold' AND target_type = actor_type
1233
+ AND target_id = actor_id AND payload = 'null' AND caller::jsonb->>'source' = 'cold'
1234
+ AND caller::jsonb->>'_tag' = 'System') IS TRUE))`;
1235
+ yield* sql `ALTER TABLE actor_placements ADD COLUMN state_version integer NOT NULL DEFAULT 0`;
1236
+ }));
1237
+ yield* sql `CREATE INDEX actor_generations_cold_version
1238
+ ON actor_generations (actor_type, cold_state_version) WHERE cold_ref IS NOT NULL`;
1239
+ yield* sql `CREATE TABLE actor_cold_garbage (
1240
+ routing_key bigint NOT NULL,
1241
+ tenant_id text NOT NULL,
1242
+ actor_type text NOT NULL,
1243
+ actor_id text NOT NULL,
1244
+ object_key text NOT NULL,
1245
+ unreferenced_at_ms bigint NOT NULL,
1246
+ PRIMARY KEY (routing_key, object_key)
1247
+ )`;
1248
+ yield* sql `CREATE INDEX actor_cold_garbage_due ON actor_cold_garbage (unreferenced_at_ms)`;
1249
+ yield* sql `ALTER TABLE actor_cold_garbage ENABLE ROW LEVEL SECURITY`;
1250
+ yield* sql `CREATE POLICY durable_tenant ON actor_cold_garbage
1251
+ USING (tenant_id = current_setting('durable.tenant', true))
1252
+ WITH CHECK (tenant_id = current_setting('durable.tenant', true))`;
1253
+ yield* sql `CREATE OR REPLACE VIEW durable.actors AS
1254
+ SELECT g.tenant_id, g.actor_type, g.actor_id, g.routing_key, p.placement,
1255
+ g.generation, g.created, g.event_sequence AS last_event_sequence,
1256
+ g.cold_ref IS NOT NULL AS cold
1257
+ FROM actor_generations g
1258
+ LEFT JOIN actor_placements p ON p.actor_type = g.actor_type`;
1259
+ }),
1260
+ /** The transaction's generation lock, not a referential trigger, authorizes state and receipt writes. */
1261
+ "0035_hot_table_foreign_keys": Effect.gen(function* () {
1262
+ const sql = yield* SqlClient.SqlClient;
1263
+ yield* briefLocks(Effect.gen(function* () {
1264
+ for (const table of ["actor_state", "actor_receipts"]) {
1265
+ const constraints = yield* sql `
1266
+ SELECT c.conname AS name FROM pg_constraint c
1267
+ WHERE c.contype = 'f' AND c.conrelid = ${table}::regclass
1268
+ AND c.confrelid = 'actor_generations'::regclass
1269
+ AND c.confupdtype = 'a' AND c.confdeltype = 'a' AND c.confmatchtype = 's'
1270
+ AND c.convalidated AND NOT c.condeferrable AND NOT c.condeferred
1271
+ AND ARRAY(SELECT a.attname::text FROM unnest(c.conkey) WITH ORDINALITY k(n, ord)
1272
+ JOIN pg_attribute a ON a.attrelid = c.conrelid AND a.attnum = k.n
1273
+ ORDER BY k.ord) = ARRAY['routing_key', 'tenant_id', 'actor_type', 'actor_id']
1274
+ AND ARRAY(SELECT a.attname::text FROM unnest(c.confkey) WITH ORDINALITY k(n, ord)
1275
+ JOIN pg_attribute a ON a.attrelid = c.confrelid AND a.attnum = k.n
1276
+ ORDER BY k.ord) = ARRAY['routing_key', 'tenant_id', 'actor_type', 'actor_id']`;
1277
+ if (constraints.length !== 1) {
1278
+ return yield* new Migrator.MigrationError({
1279
+ kind: "BadState",
1280
+ message: `Expected exactly one generation foreign key on ${table}; found ${constraints.length}`,
1281
+ });
1282
+ }
1283
+ yield* sql `ALTER TABLE ${sql(table)} DROP CONSTRAINT ${sql(constraints[0].name)}`;
1284
+ }
1285
+ }));
1286
+ }),
1216
1287
  };
1217
1288
  /**
1218
1289
  * Runs `record` like `Migrator`, but first refuses a database where a
@@ -1246,7 +1317,7 @@ export const migrator = (record) => {
1246
1317
  const transactional = refuseSkipped.pipe(Effect.andThen(run), Effect.tap(() => refuseSkipped));
1247
1318
  return Effect.gen(function* () {
1248
1319
  return yield* Option.isSome(yield* Effect.serviceOption(PgClient.PgClient))
1249
- ? withMigrationCoordination(transactional)
1320
+ ? withMigrationCoordination({ effect: transactional, history: "actor_migrations" })
1250
1321
  : transactional;
1251
1322
  });
1252
1323
  };
@@ -65,22 +65,30 @@ export declare const takeBatch: <W extends Mergeable & {
65
65
  * front command is takeable, so the worker never sleeps past ready work and a
66
66
  * command whose hook is still running holds back every command behind it.
67
67
  * `alone` outlives the handler: it holds the ids of a failed batch, each run
68
- * in a transaction of its own once.
68
+ * in a transaction of its own once. Methods are shared; the waiting commands,
69
+ * wake-up and retry isolation belong to this handler alone.
69
70
  */
70
71
  export declare const activationMailbox: <W extends Mergeable & {
71
72
  queued: boolean;
72
- }>(alone: Set<string>) => {
73
- offer: (entry: W) => void;
73
+ }>(alone: Set<string>) => ActivationMailbox<W>;
74
+ declare class ActivationMailbox<W extends Mergeable & {
75
+ queued: boolean;
76
+ }> {
77
+ private readonly alone;
78
+ private readonly waiting;
79
+ private readonly ready;
80
+ constructor(alone: Set<string>);
81
+ offer(entry: W): void;
74
82
  /** Marks `entry`'s `queued` hook finished, so a batch may take it. */
75
- queued: (entry: W) => void;
83
+ queued(entry: W): void;
76
84
  /** Waits until the front command may be takeable. */
77
- await: import("effect/Effect").Effect<void, never, never>;
85
+ readonly await: import("effect/Effect").Effect<void, never, never>;
78
86
  /** The next batch, possibly empty; the wake-up closes once the front command is not takeable. */
79
- take: () => W[];
87
+ take(): W[];
80
88
  /** Puts taken commands back in front of everything that arrived since. */
81
- requeue: (entries: ReadonlyArray<W>) => void;
89
+ requeue(entries: ReadonlyArray<W>): void;
82
90
  /** Runs each of `ids` alone once, the next time it is taken. */
83
- isolate: (ids: Iterable<string>) => void;
84
- size: () => number;
85
- };
91
+ isolate(ids: Iterable<string>): void;
92
+ size(): number;
93
+ }
86
94
  export {};
@@ -84,38 +84,46 @@ export const takeBatch = ({ waiting, alone, }) => {
84
84
  * front command is takeable, so the worker never sleeps past ready work and a
85
85
  * command whose hook is still running holds back every command behind it.
86
86
  * `alone` outlives the handler: it holds the ids of a failed batch, each run
87
- * in a transaction of its own once.
87
+ * in a transaction of its own once. Methods are shared; the waiting commands,
88
+ * wake-up and retry isolation belong to this handler alone.
88
89
  */
89
- export const activationMailbox = (alone) => {
90
- const waiting = [];
91
- const ready = Latch.makeUnsafe(false);
92
- return {
93
- offer: (entry) => {
94
- waiting.push(entry);
95
- },
96
- /** Marks `entry`'s `queued` hook finished, so a batch may take it. */
97
- queued: (entry) => {
98
- entry.queued = true;
99
- ready.openUnsafe();
100
- },
101
- /** Waits until the front command may be takeable. */
102
- await: ready.await,
103
- /** The next batch, possibly empty; the wake-up closes once the front command is not takeable. */
104
- take: () => {
105
- const batch = takeBatch({ waiting, alone });
106
- if (waiting[0]?.queued !== true)
107
- ready.closeUnsafe();
108
- return batch;
109
- },
110
- /** Puts taken commands back in front of everything that arrived since. */
111
- requeue: (entries) => {
112
- if (entries.length === 0)
113
- return;
114
- waiting.unshift(...entries);
115
- ready.openUnsafe();
116
- },
117
- /** Runs each of `ids` alone once, the next time it is taken. */
118
- isolate: (ids) => markAlone({ alone, ids }),
119
- size: () => waiting.length,
120
- };
121
- };
90
+ export const activationMailbox = (alone) => new ActivationMailbox(alone);
91
+ class ActivationMailbox {
92
+ alone;
93
+ waiting = [];
94
+ ready = Latch.makeUnsafe(false);
95
+ constructor(alone) {
96
+ this.alone = alone;
97
+ }
98
+ offer(entry) {
99
+ this.waiting.push(entry);
100
+ }
101
+ /** Marks `entry`'s `queued` hook finished, so a batch may take it. */
102
+ queued(entry) {
103
+ entry.queued = true;
104
+ this.ready.openUnsafe();
105
+ }
106
+ /** Waits until the front command may be takeable. */
107
+ await = this.ready.await;
108
+ /** The next batch, possibly empty; the wake-up closes once the front command is not takeable. */
109
+ take() {
110
+ const batch = takeBatch({ waiting: this.waiting, alone: this.alone });
111
+ if (this.waiting[0]?.queued !== true)
112
+ this.ready.closeUnsafe();
113
+ return batch;
114
+ }
115
+ /** Puts taken commands back in front of everything that arrived since. */
116
+ requeue(entries) {
117
+ if (entries.length === 0)
118
+ return;
119
+ this.waiting.unshift(...entries);
120
+ this.ready.openUnsafe();
121
+ }
122
+ /** Runs each of `ids` alone once, the next time it is taken. */
123
+ isolate(ids) {
124
+ markAlone({ alone: this.alone, ids });
125
+ }
126
+ size() {
127
+ return this.waiting.length;
128
+ }
129
+ }
@@ -42,7 +42,7 @@ export declare const commandEntity: (name: string) => Entity.Entity<string, Rpc.
42
42
  readonly caller: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"User", {
43
43
  readonly subject: Schema.NonEmptyString;
44
44
  }>, Schema.TaggedStruct<"Anonymous", {}>, Schema.TaggedStruct<"System", {
45
- readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription"]>;
45
+ readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription", "cold"]>;
46
46
  readonly ref: Schema.optional<Schema.Struct<{
47
47
  readonly tenant: Schema.NonEmptyString;
48
48
  readonly actor: Schema.NonEmptyString;
@@ -147,7 +147,7 @@ export declare const connectionEntity: (name: string) => Entity.Entity<`${string
147
147
  caller: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"User", {
148
148
  readonly subject: Schema.NonEmptyString;
149
149
  }>, Schema.TaggedStruct<"Anonymous", {}>, Schema.TaggedStruct<"System", {
150
- readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription"]>;
150
+ readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription", "cold"]>;
151
151
  readonly ref: Schema.optional<Schema.Struct<{
152
152
  readonly tenant: Schema.NonEmptyString;
153
153
  readonly actor: Schema.NonEmptyString;
@@ -224,7 +224,7 @@ export declare const connectionEntity: (name: string) => Entity.Entity<`${string
224
224
  caller: Schema.toTaggedUnion<"_tag", readonly [Schema.TaggedStruct<"User", {
225
225
  readonly subject: Schema.NonEmptyString;
226
226
  }>, Schema.TaggedStruct<"Anonymous", {}>, Schema.TaggedStruct<"System", {
227
- readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription"]>;
227
+ readonly source: Schema.Literals<readonly ["process", "actor", "timer", "cron", "workflow", "job", "subscription", "cold"]>;
228
228
  readonly ref: Schema.optional<Schema.Struct<{
229
229
  readonly tenant: Schema.NonEmptyString;
230
230
  readonly actor: Schema.NonEmptyString;