@rikalabs/akter 0.1.0-alpha.0 → 0.1.0-alpha.3

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 (308) hide show
  1. package/CHANGELOG.md +46 -3
  2. package/README.md +2 -2
  3. package/dist/actor/definition.d.ts +13 -9
  4. package/dist/actor/descriptor.d.ts +2 -2
  5. package/dist/actor/descriptor.js +4 -4
  6. package/dist/client/calls.js +1 -1
  7. package/dist/client/index.d.ts +1 -1
  8. package/dist/client/index.js +1 -1
  9. package/dist/errors/actor.d.ts +12 -42
  10. package/dist/errors/actor.js +13 -42
  11. package/dist/index.d.ts +1 -1
  12. package/dist/index.js +1 -1
  13. package/dist/members/payload.d.ts +1 -1
  14. package/dist/protocol/inspection.d.ts +221 -1
  15. package/dist/protocol/inspection.js +151 -2
  16. package/dist/protocol/wire.d.ts +6 -49
  17. package/dist/protocol/wire.js +1 -10
  18. package/dist/runtime/actors.d.ts +3 -0
  19. package/dist/runtime/adoption/backfill.js +1 -1
  20. package/dist/runtime/adoption/enforce.js +2 -2
  21. package/dist/runtime/adoption/observe.js +1 -1
  22. package/dist/runtime/adoption/plan.d.ts +1 -1
  23. package/dist/runtime/adoption/startup.js +1 -1
  24. package/dist/runtime/adoption/status.d.ts +1 -1
  25. package/dist/runtime/app.d.ts +65 -0
  26. package/dist/runtime/app.js +38 -0
  27. package/dist/runtime/connections/holder.d.ts +11 -1
  28. package/dist/runtime/connections/holder.js +8 -4
  29. package/dist/runtime/connections/streams.d.ts +4 -4
  30. package/dist/runtime/content/store.js +15 -11
  31. package/dist/runtime/database/bounded.d.ts +4 -18
  32. package/dist/runtime/database/bounded.js +36 -22
  33. package/dist/runtime/database/coordination.d.ts +12 -3
  34. package/dist/runtime/database/coordination.js +29 -36
  35. package/dist/runtime/database/migrations.d.ts +61 -29
  36. package/dist/runtime/database/migrations.js +375 -139
  37. package/dist/runtime/database/replica.d.ts +4 -1
  38. package/dist/runtime/database/replica.js +4 -1
  39. package/dist/runtime/database/schema.d.ts +13 -0
  40. package/dist/runtime/database/schema.js +13 -0
  41. package/dist/runtime/database/shards.d.ts +31 -13
  42. package/dist/runtime/database/shards.js +38 -26
  43. package/dist/runtime/entity/register.d.ts +8 -1
  44. package/dist/runtime/entity/register.js +58 -7
  45. package/dist/runtime/fleet/checks.js +4 -4
  46. package/dist/runtime/fleet/maintainer.d.ts +1 -1
  47. package/dist/runtime/fleet/maintainer.js +1 -1
  48. package/dist/runtime/index.d.ts +2 -0
  49. package/dist/runtime/index.js +1 -0
  50. package/dist/runtime/inspector/layer.d.ts +20 -0
  51. package/dist/runtime/inspector/layer.js +224 -5
  52. package/dist/runtime/inspector/queries.d.ts +54 -8
  53. package/dist/runtime/inspector/queries.js +84 -15
  54. package/dist/runtime/layer.d.ts +48 -39
  55. package/dist/runtime/layer.js +124 -56
  56. package/dist/runtime/operators/audit.d.ts +3 -0
  57. package/dist/runtime/operators/audit.js +6 -2
  58. package/dist/runtime/operators/repair.js +30 -10
  59. package/dist/runtime/operators/seed.js +4 -2
  60. package/dist/runtime/payloads/versions.d.ts +4 -4
  61. package/dist/runtime/payloads/versions.js +24 -15
  62. package/dist/runtime/queries.js +4 -2
  63. package/dist/runtime/registration.d.ts +5 -2
  64. package/dist/runtime/registration.js +3 -3
  65. package/dist/runtime/runner.d.ts +12 -0
  66. package/dist/runtime/runner.js +12 -0
  67. package/dist/runtime/storage/codec.d.ts +19 -7
  68. package/dist/runtime/storage/codec.js +23 -7
  69. package/dist/runtime/storage/placements.d.ts +6 -1
  70. package/dist/runtime/storage/placements.js +98 -6
  71. package/dist/runtime/storage/retention.d.ts +6 -0
  72. package/dist/runtime/storage/retention.js +25 -13
  73. package/dist/runtime/subscriptions/relay.js +47 -44
  74. package/dist/runtime/subscriptions/storage.d.ts +3 -2
  75. package/dist/runtime/subscriptions/storage.js +9 -6
  76. package/dist/runtime/telemetry/live.d.ts +255 -0
  77. package/dist/runtime/telemetry/live.js +540 -0
  78. package/dist/runtime/telemetry/metrics.d.ts +2 -0
  79. package/dist/runtime/telemetry/metrics.js +4 -1
  80. package/dist/runtime/telemetry/sampler.js +33 -13
  81. package/dist/runtime/telemetry/spans.d.ts +1 -1
  82. package/dist/runtime/telemetry/spans.js +1 -1
  83. package/dist/runtime/topology/locks.d.ts +6 -3
  84. package/dist/runtime/topology/locks.js +27 -4
  85. package/dist/runtime/turn/execute.d.ts +2 -0
  86. package/dist/runtime/turn/execute.js +5 -2
  87. package/dist/runtime/turn/pipeline.d.ts +5 -12
  88. package/dist/runtime/turn/pipeline.js +11 -11
  89. package/dist/runtime/turn/relay.d.ts +13 -3
  90. package/dist/runtime/turn/relay.js +27 -14
  91. package/dist/runtime/turn/rows.js +4 -4
  92. package/dist/runtime/workflows/compatibility.d.ts +1 -1
  93. package/dist/runtime/workflows/compatibility.js +85 -31
  94. package/dist/runtime/workflows/engine.js +20 -17
  95. package/dist/serve/layer.js +3 -2
  96. package/dist/tables/owned.d.ts +1 -1
  97. package/dist/tables/owned.js +1 -1
  98. package/dist/testing/actor-test.d.ts +2 -24
  99. package/dist/testing/actor-test.js +1 -27
  100. package/dist/testing/index.d.ts +0 -12
  101. package/dist/testing/index.js +0 -6
  102. package/package.json +1 -1
  103. package/dist/runtime/database/neki/migrations.d.ts +0 -31
  104. package/dist/runtime/database/neki/migrations.js +0 -276
  105. package/dist/runtime/database/neki/session.d.ts +0 -23
  106. package/dist/runtime/database/neki/session.js +0 -31
  107. package/dist/testing/cluster.d.ts +0 -93
  108. package/dist/testing/cluster.js +0 -296
  109. package/dist/testing/conformance/access.d.ts +0 -5
  110. package/dist/testing/conformance/access.js +0 -531
  111. package/dist/testing/conformance/admission.d.ts +0 -11
  112. package/dist/testing/conformance/admission.js +0 -182
  113. package/dist/testing/conformance/adoption.d.ts +0 -2
  114. package/dist/testing/conformance/adoption.js +0 -1019
  115. package/dist/testing/conformance/assertions.d.ts +0 -202
  116. package/dist/testing/conformance/assertions.js +0 -849
  117. package/dist/testing/conformance/batches.d.ts +0 -41
  118. package/dist/testing/conformance/batches.js +0 -388
  119. package/dist/testing/conformance/blobs.d.ts +0 -14
  120. package/dist/testing/conformance/blobs.js +0 -405
  121. package/dist/testing/conformance/capacity.d.ts +0 -8
  122. package/dist/testing/conformance/capacity.js +0 -433
  123. package/dist/testing/conformance/client.d.ts +0 -5
  124. package/dist/testing/conformance/client.js +0 -719
  125. package/dist/testing/conformance/cold-serve.d.ts +0 -16
  126. package/dist/testing/conformance/cold-serve.js +0 -254
  127. package/dist/testing/conformance/connections/actors.d.ts +0 -1143
  128. package/dist/testing/conformance/connections/actors.js +0 -180
  129. package/dist/testing/conformance/connections/harness.d.ts +0 -1754
  130. package/dist/testing/conformance/connections/harness.js +0 -142
  131. package/dist/testing/conformance/connections/holders.d.ts +0 -4
  132. package/dist/testing/conformance/connections/holders.js +0 -165
  133. package/dist/testing/conformance/connections/lifecycle.d.ts +0 -4
  134. package/dist/testing/conformance/connections/lifecycle.js +0 -113
  135. package/dist/testing/conformance/connections/limits.d.ts +0 -4
  136. package/dist/testing/conformance/connections/limits.js +0 -127
  137. package/dist/testing/conformance/connections/reauthorization.d.ts +0 -4
  138. package/dist/testing/conformance/connections/reauthorization.js +0 -170
  139. package/dist/testing/conformance/connections/resync.d.ts +0 -4
  140. package/dist/testing/conformance/connections/resync.js +0 -360
  141. package/dist/testing/conformance/connections/takeover.d.ts +0 -4
  142. package/dist/testing/conformance/connections/takeover.js +0 -287
  143. package/dist/testing/conformance/connections.d.ts +0 -4
  144. package/dist/testing/conformance/connections.js +0 -15
  145. package/dist/testing/conformance/content-blobs.d.ts +0 -16
  146. package/dist/testing/conformance/content-blobs.js +0 -534
  147. package/dist/testing/conformance/counter.d.ts +0 -16
  148. package/dist/testing/conformance/counter.js +0 -813
  149. package/dist/testing/conformance/cron.d.ts +0 -5
  150. package/dist/testing/conformance/cron.js +0 -665
  151. package/dist/testing/conformance/drain.d.ts +0 -26
  152. package/dist/testing/conformance/drain.js +0 -540
  153. package/dist/testing/conformance/effect-control.d.ts +0 -38
  154. package/dist/testing/conformance/effect-control.js +0 -1059
  155. package/dist/testing/conformance/effects.d.ts +0 -23
  156. package/dist/testing/conformance/effects.js +0 -432
  157. package/dist/testing/conformance/events.d.ts +0 -28
  158. package/dist/testing/conformance/events.js +0 -335
  159. package/dist/testing/conformance/export.d.ts +0 -2
  160. package/dist/testing/conformance/export.js +0 -368
  161. package/dist/testing/conformance/fleet.d.ts +0 -444
  162. package/dist/testing/conformance/fleet.js +0 -1009
  163. package/dist/testing/conformance/groups.d.ts +0 -7
  164. package/dist/testing/conformance/groups.js +0 -353
  165. package/dist/testing/conformance/heap.d.ts +0 -3
  166. package/dist/testing/conformance/heap.js +0 -149
  167. package/dist/testing/conformance/http.d.ts +0 -2809
  168. package/dist/testing/conformance/http.js +0 -1234
  169. package/dist/testing/conformance/inspection-views.d.ts +0 -8
  170. package/dist/testing/conformance/inspection-views.js +0 -272
  171. package/dist/testing/conformance/inspector.d.ts +0 -7
  172. package/dist/testing/conformance/inspector.js +0 -650
  173. package/dist/testing/conformance/mint.d.ts +0 -58
  174. package/dist/testing/conformance/mint.js +0 -692
  175. package/dist/testing/conformance/multi-runner.d.ts +0 -8
  176. package/dist/testing/conformance/multi-runner.js +0 -403
  177. package/dist/testing/conformance/neki/plan.d.ts +0 -19
  178. package/dist/testing/conformance/neki/plan.js +0 -30
  179. package/dist/testing/conformance/observability.d.ts +0 -3
  180. package/dist/testing/conformance/observability.js +0 -343
  181. package/dist/testing/conformance/offline.d.ts +0 -4
  182. package/dist/testing/conformance/offline.js +0 -474
  183. package/dist/testing/conformance/operator-harness.d.ts +0 -57
  184. package/dist/testing/conformance/operator-harness.js +0 -78
  185. package/dist/testing/conformance/operator.d.ts +0 -3
  186. package/dist/testing/conformance/operator.js +0 -379
  187. package/dist/testing/conformance/outbox-cross-shard.d.ts +0 -14
  188. package/dist/testing/conformance/outbox-cross-shard.js +0 -160
  189. package/dist/testing/conformance/outbox.d.ts +0 -36
  190. package/dist/testing/conformance/outbox.js +0 -437
  191. package/dist/testing/conformance/payload-migrations.d.ts +0 -3
  192. package/dist/testing/conformance/payload-migrations.js +0 -612
  193. package/dist/testing/conformance/pipeline.d.ts +0 -3
  194. package/dist/testing/conformance/pipeline.js +0 -1040
  195. package/dist/testing/conformance/placement.d.ts +0 -22
  196. package/dist/testing/conformance/placement.js +0 -539
  197. package/dist/testing/conformance/platform.d.ts +0 -40
  198. package/dist/testing/conformance/platform.js +0 -101
  199. package/dist/testing/conformance/progress.d.ts +0 -39
  200. package/dist/testing/conformance/progress.js +0 -666
  201. package/dist/testing/conformance/properties.d.ts +0 -6
  202. package/dist/testing/conformance/properties.js +0 -295
  203. package/dist/testing/conformance/protocols.d.ts +0 -4
  204. package/dist/testing/conformance/protocols.js +0 -618
  205. package/dist/testing/conformance/provider/advertise.d.ts +0 -21
  206. package/dist/testing/conformance/provider/advertise.js +0 -44
  207. package/dist/testing/conformance/read-your-writes.d.ts +0 -20
  208. package/dist/testing/conformance/read-your-writes.js +0 -192
  209. package/dist/testing/conformance/reducers.d.ts +0 -8
  210. package/dist/testing/conformance/reducers.js +0 -258
  211. package/dist/testing/conformance/relay.d.ts +0 -42
  212. package/dist/testing/conformance/relay.js +0 -1115
  213. package/dist/testing/conformance/restore.d.ts +0 -43
  214. package/dist/testing/conformance/restore.js +0 -341
  215. package/dist/testing/conformance/retention.d.ts +0 -14
  216. package/dist/testing/conformance/retention.js +0 -463
  217. package/dist/testing/conformance/rls.d.ts +0 -24
  218. package/dist/testing/conformance/rls.js +0 -576
  219. package/dist/testing/conformance/simulation.d.ts +0 -31
  220. package/dist/testing/conformance/simulation.js +0 -167
  221. package/dist/testing/conformance/single-shard.d.ts +0 -2
  222. package/dist/testing/conformance/single-shard.js +0 -125
  223. package/dist/testing/conformance/singleton.d.ts +0 -3
  224. package/dist/testing/conformance/singleton.js +0 -293
  225. package/dist/testing/conformance/statements.d.ts +0 -60
  226. package/dist/testing/conformance/statements.js +0 -108
  227. package/dist/testing/conformance/streams.d.ts +0 -7
  228. package/dist/testing/conformance/streams.js +0 -280
  229. package/dist/testing/conformance/subscriptions/access.d.ts +0 -4
  230. package/dist/testing/conformance/subscriptions/access.js +0 -98
  231. package/dist/testing/conformance/subscriptions/actors.d.ts +0 -4910
  232. package/dist/testing/conformance/subscriptions/actors.js +0 -372
  233. package/dist/testing/conformance/subscriptions/cluster.d.ts +0 -4
  234. package/dist/testing/conformance/subscriptions/cluster.js +0 -89
  235. package/dist/testing/conformance/subscriptions/delivery.d.ts +0 -4
  236. package/dist/testing/conformance/subscriptions/delivery.js +0 -253
  237. package/dist/testing/conformance/subscriptions/deploys.d.ts +0 -4
  238. package/dist/testing/conformance/subscriptions/deploys.js +0 -162
  239. package/dist/testing/conformance/subscriptions/epochs.d.ts +0 -4
  240. package/dist/testing/conformance/subscriptions/epochs.js +0 -169
  241. package/dist/testing/conformance/subscriptions/failures.d.ts +0 -4
  242. package/dist/testing/conformance/subscriptions/failures.js +0 -108
  243. package/dist/testing/conformance/subscriptions/feeds.d.ts +0 -4
  244. package/dist/testing/conformance/subscriptions/feeds.js +0 -218
  245. package/dist/testing/conformance/subscriptions/harness.d.ts +0 -124
  246. package/dist/testing/conformance/subscriptions/harness.js +0 -143
  247. package/dist/testing/conformance/subscriptions/operator.d.ts +0 -4
  248. package/dist/testing/conformance/subscriptions/operator.js +0 -128
  249. package/dist/testing/conformance/subscriptions/retention.d.ts +0 -4
  250. package/dist/testing/conformance/subscriptions/retention.js +0 -288
  251. package/dist/testing/conformance/subscriptions/routing.d.ts +0 -4
  252. package/dist/testing/conformance/subscriptions/routing.js +0 -179
  253. package/dist/testing/conformance/subscriptions.d.ts +0 -4
  254. package/dist/testing/conformance/subscriptions.js +0 -19
  255. package/dist/testing/conformance/tables.d.ts +0 -125
  256. package/dist/testing/conformance/tables.js +0 -664
  257. package/dist/testing/conformance/transports/actors.d.ts +0 -1907
  258. package/dist/testing/conformance/transports/actors.js +0 -163
  259. package/dist/testing/conformance/transports/client.d.ts +0 -3
  260. package/dist/testing/conformance/transports/client.js +0 -237
  261. package/dist/testing/conformance/transports/feeds.d.ts +0 -3
  262. package/dist/testing/conformance/transports/feeds.js +0 -232
  263. package/dist/testing/conformance/transports/harness.d.ts +0 -93
  264. package/dist/testing/conformance/transports/harness.js +0 -84
  265. package/dist/testing/conformance/transports/streams.d.ts +0 -3
  266. package/dist/testing/conformance/transports/streams.js +0 -96
  267. package/dist/testing/conformance/transports/websocket.d.ts +0 -3
  268. package/dist/testing/conformance/transports/websocket.js +0 -384
  269. package/dist/testing/conformance/transports/wire.d.ts +0 -141
  270. package/dist/testing/conformance/transports/wire.js +0 -204
  271. package/dist/testing/conformance/transports.d.ts +0 -3
  272. package/dist/testing/conformance/transports.js +0 -11
  273. package/dist/testing/conformance/watch.d.ts +0 -59
  274. package/dist/testing/conformance/watch.js +0 -879
  275. package/dist/testing/conformance/workflow-engine.d.ts +0 -97
  276. package/dist/testing/conformance/workflow-engine.js +0 -253
  277. package/dist/testing/conformance/workflow-versions.d.ts +0 -3
  278. package/dist/testing/conformance/workflow-versions.js +0 -545
  279. package/dist/testing/conformance/workflows/access.d.ts +0 -4
  280. package/dist/testing/conformance/workflows/access.js +0 -147
  281. package/dist/testing/conformance/workflows/actors.d.ts +0 -2317
  282. package/dist/testing/conformance/workflows/actors.js +0 -305
  283. package/dist/testing/conformance/workflows/basics.d.ts +0 -4
  284. package/dist/testing/conformance/workflows/basics.js +0 -144
  285. package/dist/testing/conformance/workflows/engine.d.ts +0 -4
  286. package/dist/testing/conformance/workflows/engine.js +0 -80
  287. package/dist/testing/conformance/workflows/guards.d.ts +0 -4
  288. package/dist/testing/conformance/workflows/guards.js +0 -172
  289. package/dist/testing/conformance/workflows/harness.d.ts +0 -14
  290. package/dist/testing/conformance/workflows/harness.js +0 -44
  291. package/dist/testing/conformance/workflows/recovery.d.ts +0 -4
  292. package/dist/testing/conformance/workflows/recovery.js +0 -181
  293. package/dist/testing/conformance/workflows/starts.d.ts +0 -4
  294. package/dist/testing/conformance/workflows/starts.js +0 -57
  295. package/dist/testing/conformance/workflows/suspension.d.ts +0 -4
  296. package/dist/testing/conformance/workflows/suspension.js +0 -106
  297. package/dist/testing/conformance/workflows/waits.d.ts +0 -4
  298. package/dist/testing/conformance/workflows/waits.js +0 -284
  299. package/dist/testing/conformance/workflows.d.ts +0 -4
  300. package/dist/testing/conformance/workflows.js +0 -19
  301. package/dist/testing/conformance.d.ts +0 -330
  302. package/dist/testing/conformance.js +0 -364
  303. package/dist/testing/foundation.d.ts +0 -41
  304. package/dist/testing/foundation.js +0 -517
  305. package/dist/testing/simulate-cluster.d.ts +0 -112
  306. package/dist/testing/simulate-cluster.js +0 -257
  307. package/dist/testing/simulate.d.ts +0 -90
  308. package/dist/testing/simulate.js +0 -127
@@ -1,813 +0,0 @@
1
- import { Cause, Clock, Deferred, Effect, Exit, Fiber, Layer, Schedule, Schema } from "effect";
2
- import { SqlClient, Statement } from "effect/sql";
3
- import { Actor, Actors, CurrentCaller, User } from "../../index.js";
4
- import { CommandConflict, CommandExpired, InvalidCommandId, Unauthorized, } from "../../errors/actor.js";
5
- import { checkIdentity, databaseTime } from "../../runtime/turn/admission.js";
6
- import { routingKey } from "../../runtime/storage/codec.js";
7
- import { ActorTest } from "../actor-test.js";
8
- import { payloadHash } from "./admission.js";
9
- class Rejected extends Schema.TaggedError()("Rejected", { amount: Schema.Finite }) {
10
- }
11
- const Increment = Actor.command("Increment", { payload: Schema.Finite, success: Schema.Finite });
12
- const Reject = Actor.command("Reject", { payload: Schema.Finite, error: Rejected });
13
- const Nested = Actor.command("Nested");
14
- const Escape = Actor.command("Escape");
15
- const Hold = Actor.command("Hold");
16
- const Steal = Actor.command("Steal");
17
- class Forbidden extends Schema.TaggedError()("Forbidden", { below: Schema.Finite }) {
18
- }
19
- const Count = Actor.query("Count", { success: Schema.Finite });
20
- const AtLeast = Actor.query("AtLeast", {
21
- payload: Schema.Finite,
22
- success: Schema.Finite,
23
- error: Forbidden,
24
- });
25
- const Counter = Actor.make("Counter", {
26
- key: Schema.String,
27
- state: Actor.state({ count: Schema.Finite.pipe(Schema.withDecodingDefault(Effect.succeed(0))) }),
28
- api: { Increment, Reject, Nested, Escape, Hold, Steal, Count, AtLeast },
29
- });
30
- const CounterReads = (fixture) => Counter.toQueryLayer(Effect.succeed({
31
- Count: Effect.fnUntraced(function* () {
32
- fixture.queries += 1;
33
- yield* fixture.duringQuery.pipe(Effect.orDie);
34
- return (yield* Counter.Read).state.count;
35
- }),
36
- AtLeast: Effect.fnUntraced(function* (minimum) {
37
- const count = (yield* Counter.Read).state.count;
38
- if (count < minimum)
39
- return yield* Forbidden.make({ below: minimum });
40
- return count;
41
- }),
42
- }));
43
- const CounterLive = (fixture) => Counter.toLayer(Effect.succeed({
44
- Increment: Effect.fnUntraced(function* (amount) {
45
- const turn = yield* Counter.Turn;
46
- fixture.executions += 1;
47
- yield* turn.state.set({ count: turn.state.count + amount });
48
- return turn.state.count;
49
- }),
50
- Reject: Effect.fnUntraced(function* (amount) {
51
- const turn = yield* Counter.Turn;
52
- fixture.executions += 1;
53
- yield* turn.state.set({ count: 999 });
54
- return yield* Rejected.make({ amount });
55
- }),
56
- Nested: Effect.fnUntraced(function* () {
57
- const turn = yield* Counter.Turn;
58
- yield* turn.state.set({ count: 99 });
59
- yield* fixture.captured.pipe(Effect.orDie);
60
- }),
61
- Escape: Effect.fnUntraced(function* () {
62
- const turn = yield* Counter.Turn;
63
- fixture.escaped = turn.state.set({ count: 1000 });
64
- yield* turn.state.set({ count: 3 });
65
- }),
66
- Hold: Effect.fnUntraced(function* () {
67
- const turn = yield* Counter.Turn;
68
- fixture.escaped = turn.state.set({ count: 1000 });
69
- yield* fixture.holdHandler;
70
- yield* turn.state.set({ count: 3 });
71
- }),
72
- Steal: () => Effect.suspend(() => fixture.escaped),
73
- }));
74
- /** The Counter actor's handlers and queries, which count their runs and run what a case plants. */
75
- export const counterSuite = {
76
- fixture: () => ({
77
- executions: 0,
78
- queries: 0,
79
- captured: Effect.succeed(0),
80
- escaped: Effect.void,
81
- holdHandler: Effect.void,
82
- duringQuery: Effect.void,
83
- }),
84
- layer: (fixture) => Layer.merge(CounterLive(fixture), CounterReads(fixture)),
85
- };
86
- /** Durable-turn cases written against the Counter actor. */
87
- export const counterConformance = [
88
- {
89
- name: "commits state and receipt, replays an identical command effect, and keeps its generation",
90
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
91
- const test = yield* ActorTest;
92
- const counter = yield* Counter.get("replay");
93
- expect(yield* test.inspect(counter.ref)).toEqual({
94
- generation: undefined,
95
- state: {},
96
- receipts: 0,
97
- events: 0,
98
- outbox: 0,
99
- jobs: 0,
100
- });
101
- const increment = counter.Increment(7);
102
- expect(yield* increment).toBe(7);
103
- expect(yield* increment).toBe(7);
104
- expect(yield* counter.Increment(3)).toBe(10);
105
- expect(yield* test.inspect(counter.ref)).toEqual({
106
- generation: "1",
107
- state: { count: 10 },
108
- receipts: 2,
109
- events: 0,
110
- outbox: 0,
111
- jobs: 0,
112
- });
113
- })),
114
- },
115
- {
116
- name: "rolls back declared failures and replays their class and payload without executing again",
117
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
118
- const test = yield* ActorTest;
119
- const counter = yield* Counter.get("failure");
120
- expect(yield* counter.Increment(5)).toBe(5);
121
- const rejected = counter.Reject(13);
122
- const before = fixture.executions;
123
- const first = yield* rejected.pipe(Effect.flip);
124
- expect(first).toBeInstanceOf(Rejected);
125
- expect(first).toEqual(Rejected.make({ amount: 13 }));
126
- expect(yield* rejected.pipe(Effect.flip)).toEqual(first);
127
- expect(fixture.executions - before).toBe(1);
128
- expect(yield* test.inspect(counter.ref)).toEqual({
129
- generation: "1",
130
- state: { count: 5 },
131
- receipts: 2,
132
- events: 0,
133
- outbox: 0,
134
- jobs: 0,
135
- });
136
- })),
137
- },
138
- {
139
- name: "deduplicates concurrent deliveries and rejects changed input or command",
140
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
141
- const test = yield* ActorTest;
142
- const counter = yield* Counter.get("concurrent");
143
- const id = yield* (yield* Actors).mintCommandId;
144
- const before = fixture.executions;
145
- expect(yield* Effect.forEach([counter.Increment(11), counter.Increment(11)], (call) => call.pipe(Actor.commandId(id)), { concurrency: 2 })).toEqual([11, 11]);
146
- expect(fixture.executions - before).toBe(1);
147
- expect((yield* counter.Increment(12).pipe(Actor.commandId(id), Effect.flip)).reason._tag).toBe("CommandConflict");
148
- expect(yield* counter.Reject(11).pipe(Actor.commandId(id), Effect.flip)).toMatchObject({
149
- reason: CommandConflict.make({ commandId: id }),
150
- });
151
- expect(yield* test.inspect(counter.ref)).toMatchObject({
152
- state: { count: 11 },
153
- receipts: 1,
154
- });
155
- })),
156
- },
157
- {
158
- name: "captures callers, preserves same-subject access, and never partitions deduplication by caller",
159
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
160
- const test = yield* ActorTest;
161
- const alice = yield* Counter.get("privacy");
162
- const bob = yield* Counter.get("privacy").pipe(Actor.as(User.make({ subject: "bob" })));
163
- const id = yield* (yield* Actors).mintCommandId;
164
- expect(yield* alice
165
- .Increment(17)
166
- .pipe(Actor.commandId(id), Effect.provideService(CurrentCaller, User.make({ subject: "bob" })))).toBe(17);
167
- const rotated = yield* Counter.get("privacy").pipe(Actor.as(User.make({ subject: "alice" })));
168
- expect(yield* rotated.Increment(17).pipe(Actor.commandId(id))).toBe(17);
169
- expect(yield* bob.Increment(17).pipe(Actor.commandId(id), Effect.flip)).toMatchObject({
170
- reason: Unauthorized.make({ code: "receipt_access_denied" }),
171
- });
172
- const otherTenant = yield* Counter.get("privacy").pipe(Actor.tenant("other"), Actor.as(User.make({ subject: "bob" })));
173
- expect(yield* otherTenant.Increment(29).pipe(Actor.commandId(id))).toBe(29);
174
- expect(yield* test.inspect(alice.ref)).toMatchObject({
175
- state: { count: 17 },
176
- receipts: 1,
177
- });
178
- })),
179
- },
180
- {
181
- name: "recovers beforeHandler crashes with the same command and one committed transition",
182
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
183
- const test = yield* ActorTest;
184
- const counter = yield* Counter.get("crash-beforeHandler");
185
- const before = fixture.executions;
186
- yield* test.crashNext("beforeHandler");
187
- expect(yield* counter.Increment(23)).toBe(23);
188
- expect(fixture.executions - before).toBe(1);
189
- expect(yield* test.inspect(counter.ref)).toMatchObject({
190
- state: { count: 23 },
191
- receipts: 1,
192
- });
193
- })),
194
- },
195
- {
196
- name: "recovers beforeCommit crashes with the same command and one committed transition",
197
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
198
- const test = yield* ActorTest;
199
- const counter = yield* Counter.get("crash-beforeCommit");
200
- const before = fixture.executions;
201
- yield* test.crashNext("beforeCommit");
202
- expect(yield* counter.Increment(23)).toBe(23);
203
- expect(fixture.executions - before).toBe(2);
204
- expect(yield* test.inspect(counter.ref)).toMatchObject({
205
- state: { count: 23 },
206
- receipts: 1,
207
- });
208
- })),
209
- },
210
- {
211
- name: "recovers afterCommit crashes with the same command and one committed transition",
212
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
213
- const test = yield* ActorTest;
214
- const counter = yield* Counter.get("crash-afterCommit");
215
- const before = fixture.executions;
216
- yield* test.crashNext("afterCommit");
217
- expect(yield* counter.Increment(23)).toBe(23);
218
- expect(fixture.executions - before).toBe(1);
219
- expect(yield* test.inspect(counter.ref)).toMatchObject({
220
- state: { count: 23 },
221
- receipts: 1,
222
- });
223
- })),
224
- },
225
- {
226
- name: "keeps retries prompt after many crashed turns of one actor type",
227
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
228
- const test = yield* ActorTest;
229
- for (let index = 0; index < 12; index++) {
230
- yield* test.crashNext("beforeCommit");
231
- yield* (yield* Counter.get(`crash-many-${index}`)).Increment(1);
232
- }
233
- yield* test.crashNext("beforeCommit");
234
- const started = yield* Clock.currentTimeMillis;
235
- expect(yield* (yield* Counter.get("crash-many-last")).Increment(2)).toBe(2);
236
- expect((yield* Clock.currentTimeMillis) - started < 2_000).toBe(true);
237
- })),
238
- },
239
- {
240
- name: "backs off an actor whose turn dies on every attempt, and only that actor",
241
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
242
- const test = yield* ActorTest;
243
- const crashing = yield* Counter.get("crash-always");
244
- const elapsed = (effect) => Effect.gen(function* () {
245
- const started = yield* Clock.currentTimeMillis;
246
- const value = yield* effect;
247
- return { value, ms: (yield* Clock.currentTimeMillis) - started };
248
- });
249
- for (let crash = 0; crash < 5; crash++)
250
- yield* test.crashNext("beforeCommit");
251
- const before = fixture.executions;
252
- const backedOff = yield* elapsed(crashing.Increment(1));
253
- expect(backedOff.value).toBe(1);
254
- expect(fixture.executions - before).toBe(6);
255
- expect(backedOff.ms >= 1_500).toBe(true);
256
- yield* test.crashNext("beforeCommit");
257
- const other = yield* elapsed((yield* Counter.get("crash-always-other")).Increment(2));
258
- expect(other.value).toBe(2);
259
- expect(other.ms < 1_000).toBe(true);
260
- yield* test.crashNext("beforeCommit");
261
- const reset = yield* elapsed(crashing.Increment(1));
262
- expect(reset.value).toBe(2);
263
- expect(reset.ms < 1_000).toBe(true);
264
- })),
265
- },
266
- {
267
- name: "does not cancel an accepted turn with its waiter",
268
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
269
- const test = yield* ActorTest;
270
- const counter = yield* Counter.get("disconnect");
271
- const pause = yield* test.pauseNext("beforeCommit");
272
- const call = counter.Increment(31);
273
- const waiter = yield* call.pipe(Effect.forkChild);
274
- yield* pause.reached;
275
- yield* Fiber.interrupt(waiter);
276
- yield* pause.release;
277
- expect(yield* call).toBe(31);
278
- expect(yield* test.inspect(counter.ref)).toMatchObject({
279
- state: { count: 31 },
280
- receipts: 1,
281
- });
282
- })),
283
- },
284
- {
285
- name: "keeps uncommitted state invisible to a second connection",
286
- requiresIndependentConnections: true,
287
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
288
- const test = yield* ActorTest;
289
- const counter = yield* Counter.get("disconnect-visibility");
290
- const pause = yield* test.pauseNext("beforeCommit");
291
- const call = counter.Increment(31);
292
- const waiter = yield* call.pipe(Effect.forkChild);
293
- yield* pause.reached;
294
- expect(yield* test.inspect(counter.ref)).toEqual({
295
- generation: undefined,
296
- state: {},
297
- receipts: 0,
298
- events: 0,
299
- outbox: 0,
300
- jobs: 0,
301
- });
302
- yield* Fiber.interrupt(waiter);
303
- yield* pause.release;
304
- expect(yield* call).toBe(31);
305
- expect(yield* test.inspect(counter.ref)).toMatchObject({
306
- state: { count: 31 },
307
- receipts: 1,
308
- });
309
- })),
310
- },
311
- {
312
- name: "compresses state, keys rows by routing_key, and reads state once per activation",
313
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
314
- const test = yield* ActorTest;
315
- const counter = yield* Counter.get("warm");
316
- const sql = yield* SqlClient.SqlClient;
317
- expect(yield* counter.Increment(1)).toBe(1);
318
- const stored = yield* sql `
319
- SELECT routing_key::text AS routing_key, encode(substring(value FROM 1 FOR 4), 'hex') AS magic
320
- FROM actor_state WHERE tenant_id = ${counter.ref.tenant} AND actor_id = 'warm'`;
321
- expect(stored).toEqual([
322
- {
323
- routing_key: String(routingKey({ ref: counter.ref, placement: "tenant" })),
324
- magic: "28b52ffd",
325
- },
326
- ]);
327
- const statements = [];
328
- const recorded = (effect) => effect.pipe(Effect.provideService(Statement.CurrentTransformer, (statement) => Effect.sync(() => {
329
- statements.push(statement.compile()[0]);
330
- return statement;
331
- })));
332
- for (const amount of [2, 3, 4])
333
- yield* recorded(counter.Increment(amount));
334
- expect(statements.some((text) => /INSERT INTO actor_receipts/.test(text))).toBe(true);
335
- const stateReads = statements.filter((text) => /SELECT key, value FROM actor_state/.test(text));
336
- expect(stateReads).toEqual([]);
337
- yield* test.invalidate(counter.ref);
338
- statements.length = 0;
339
- expect(yield* recorded(counter.Increment(5))).toBe(15);
340
- expect(statements.filter((text) => /SELECT key, value FROM actor_state/.test(text)).length).toBe(1);
341
- })),
342
- },
343
- {
344
- name: "queries read committed state without activating, fencing, or receipting the actor",
345
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
346
- const test = yield* ActorTest;
347
- const counter = yield* Counter.get("query-cold");
348
- expect(yield* counter.Count()).toBe(0);
349
- expect(yield* test.inspect(counter.ref)).toEqual({
350
- generation: undefined,
351
- state: {},
352
- receipts: 0,
353
- events: 0,
354
- outbox: 0,
355
- jobs: 0,
356
- });
357
- expect(yield* counter.Increment(4)).toBe(4);
358
- yield* test.invalidate(counter.ref);
359
- const generation = (yield* test.inspect(counter.ref)).generation;
360
- expect(yield* counter.Count()).toBe(4);
361
- expect(yield* counter.AtLeast(4)).toBe(4);
362
- expect(yield* counter.AtLeast(5).pipe(Effect.flip)).toEqual(Forbidden.make({ below: 5 }));
363
- expect(yield* test.inspect(counter.ref)).toMatchObject({
364
- generation,
365
- state: { count: 4 },
366
- receipts: 1,
367
- });
368
- })),
369
- },
370
- {
371
- name: "queries never observe a running turn's uncommitted state",
372
- requiresIndependentConnections: true,
373
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
374
- const test = yield* ActorTest;
375
- const counter = yield* Counter.get("query-isolation");
376
- expect(yield* counter.Increment(2)).toBe(2);
377
- const pause = yield* test.pauseNext("beforeCommit");
378
- const writer = yield* counter.Increment(40).pipe(Effect.forkChild);
379
- yield* pause.reached;
380
- expect(yield* counter.Count()).toBe(2);
381
- yield* pause.release;
382
- expect(yield* Fiber.join(writer)).toBe(42);
383
- expect(yield* counter.Count()).toBe(42);
384
- })),
385
- },
386
- {
387
- name: "applies the caller authorization to queries",
388
- run: ({ expect, environment, fixture, access }) => environment.run(Effect.gen(function* () {
389
- const counter = yield* Counter.get("query-denied");
390
- const before = fixture.queries;
391
- access.allowed = false;
392
- expect(yield* counter.Count().pipe(Effect.flip)).toMatchObject({
393
- reason: Unauthorized.make({ code: "access_denied" }),
394
- });
395
- expect(fixture.queries).toBe(before);
396
- }).pipe(Effect.ensuring(Effect.sync(() => {
397
- access.allowed = true;
398
- })))),
399
- },
400
- {
401
- name: "withholds a query result from a caller revoked while the handler ran",
402
- run: ({ expect, environment, fixture, access }) => environment.run(Effect.gen(function* () {
403
- const counter = yield* Counter.get("query-revoked");
404
- expect(yield* counter.Increment(6)).toBe(6);
405
- fixture.duringQuery = Effect.sync(() => {
406
- access.allowed = false;
407
- });
408
- const before = fixture.queries;
409
- expect(yield* counter.Count().pipe(Effect.flip)).toMatchObject({
410
- reason: Unauthorized.make({ code: "access_denied" }),
411
- });
412
- expect(fixture.queries).toBe(before + 1);
413
- }).pipe(Effect.ensuring(Effect.sync(() => {
414
- access.allowed = true;
415
- fixture.duringQuery = Effect.void;
416
- })))),
417
- },
418
- {
419
- name: "rejects request/reply calls from a query handler without writing",
420
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
421
- const test = yield* ActorTest;
422
- const counter = yield* Counter.get("query-guard");
423
- fixture.duringQuery = counter.Increment(100);
424
- const exit = yield* counter.Count().pipe(Effect.exit);
425
- expect(Exit.isFailure(exit) && Cause.pretty(exit.cause)).toContain("Request/reply inside a turn");
426
- expect(yield* test.inspect(counter.ref)).toEqual({
427
- generation: undefined,
428
- state: {},
429
- receipts: 0,
430
- events: 0,
431
- outbox: 0,
432
- jobs: 0,
433
- });
434
- }).pipe(Effect.ensuring(Effect.sync(() => {
435
- fixture.duringQuery = Effect.void;
436
- })))),
437
- },
438
- {
439
- name: "rejects a stale generation before rerunning the handler under new authority",
440
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
441
- const test = yield* ActorTest;
442
- const counter = yield* Counter.get("stale");
443
- expect(yield* counter.Increment(5)).toBe(5);
444
- yield* test.invalidate(counter.ref);
445
- const before = fixture.executions;
446
- expect(yield* counter.Increment(8)).toBe(13);
447
- expect(fixture.executions - before).toBe(1);
448
- expect(yield* test.inspect(counter.ref)).toEqual({
449
- generation: "3",
450
- state: { count: 13 },
451
- receipts: 2,
452
- events: 0,
453
- outbox: 0,
454
- jobs: 0,
455
- });
456
- })),
457
- },
458
- {
459
- name: "rolls back captured request/reply misuse and rejects escaped state capabilities",
460
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
461
- const test = yield* ActorTest;
462
- const counter = yield* Counter.get("guard");
463
- fixture.captured = counter.Increment(100);
464
- const exit = yield* counter.Nested().pipe(Effect.exit);
465
- expect(Exit.isFailure(exit) && Cause.pretty(exit.cause)).toContain("Request/reply inside a turn");
466
- expect(yield* test.inspect(counter.ref)).toEqual({
467
- generation: undefined,
468
- state: {},
469
- receipts: 0,
470
- events: 0,
471
- outbox: 0,
472
- jobs: 0,
473
- });
474
- yield* counter.Escape();
475
- const escapedExit = yield* fixture.escaped.pipe(Effect.exit);
476
- expect(Exit.isFailure(escapedExit) && Cause.pretty(escapedExit.cause)).toContain("State capability escaped its turn");
477
- expect(yield* test.inspect(counter.ref)).toMatchObject({
478
- state: { count: 3 },
479
- receipts: 1,
480
- });
481
- })),
482
- },
483
- {
484
- name: "rejects a state setter from another still-active actor turn",
485
- requiresIndependentConnections: true,
486
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
487
- const owner = yield* Counter.get("capability-owner");
488
- const thief = yield* Counter.get("capability-thief");
489
- const test = yield* ActorTest;
490
- const ready = yield* Deferred.make();
491
- const release = yield* Deferred.make();
492
- fixture.holdHandler = Deferred.succeed(ready, undefined).pipe(Effect.andThen(Deferred.await(release)));
493
- const holder = yield* owner.Hold().pipe(Effect.forkChild);
494
- yield* Deferred.await(ready);
495
- const stolen = yield* thief
496
- .Steal()
497
- .pipe(Effect.exit, Effect.ensuring(Deferred.succeed(release, undefined)));
498
- expect(Exit.isFailure(stolen) && Cause.pretty(stolen.cause)).toContain("State capability escaped its turn");
499
- yield* Fiber.join(holder);
500
- expect(yield* test.inspect(owner.ref)).toMatchObject({
501
- state: { count: 3 },
502
- receipts: 1,
503
- });
504
- expect(yield* test.inspect(thief.ref)).toEqual({
505
- generation: undefined,
506
- state: {},
507
- receipts: 0,
508
- events: 0,
509
- outbox: 0,
510
- jobs: 0,
511
- });
512
- })),
513
- },
514
- {
515
- name: "denies a competing caller admitted while the original failure is still uncommitted",
516
- requiresIndependentConnections: true,
517
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
518
- const test = yield* ActorTest;
519
- const alice = yield* Counter.get("caller-race");
520
- const bob = yield* Counter.get("caller-race").pipe(Actor.as(User.make({ subject: "bob" })));
521
- const id = yield* (yield* Actors).mintCommandId;
522
- const committing = yield* test.pauseNext("beforeCommit");
523
- const before = fixture.executions;
524
- const original = yield* alice
525
- .Reject(71)
526
- .pipe(Actor.commandId(id), Effect.flip, Effect.forkChild);
527
- yield* committing.reached;
528
- const delivering = yield* test.pauseNext("beforeDelivery");
529
- const competitor = yield* bob
530
- .Reject(71)
531
- .pipe(Actor.commandId(id), Effect.flip, Effect.forkChild);
532
- yield* delivering.reached;
533
- yield* committing.release;
534
- expect(yield* Fiber.join(original)).toEqual(Rejected.make({ amount: 71 }));
535
- yield* delivering.release;
536
- expect(yield* Fiber.join(competitor)).toMatchObject({
537
- reason: Unauthorized.make({ code: "receipt_access_denied" }),
538
- });
539
- expect(fixture.executions - before).toBe(1);
540
- expect(yield* test.inspect(alice.ref)).toMatchObject({ state: {}, receipts: 1 });
541
- })),
542
- },
543
- {
544
- name: "refuses to reinterpret retained identities under a changed retry window",
545
- run: ({ expect, environment }) => Effect.runPromise(Effect.gen(function* () {
546
- yield* environment.stop;
547
- const incompatible = environment.build({ retryWindowMs: 60_001 });
548
- const exit = yield* Effect.promise(() => incompatible.runPromiseExit(Effect.void));
549
- yield* Effect.promise(() => incompatible.dispose());
550
- yield* environment.restart;
551
- expect(Exit.isFailure(exit) && Cause.pretty(exit.cause)).toContain("differs from the deployment");
552
- })),
553
- },
554
- {
555
- name: "revokes external access without cancelling an in-flight command or its retry",
556
- run: ({ expect, environment, access }) => environment.run(Effect.gen(function* () {
557
- const test = yield* ActorTest;
558
- const counter = yield* Counter.get("revoked");
559
- const id = yield* (yield* Actors).mintCommandId;
560
- const pause = yield* test.pauseNext("beforeHandler");
561
- yield* test.crashNext("beforeCommit");
562
- const call = counter.Increment(37).pipe(Actor.commandId(id));
563
- const waiter = yield* call.pipe(Effect.result, Effect.forkChild);
564
- yield* pause.reached;
565
- access.allowed = false;
566
- yield* pause.release;
567
- const result = yield* Fiber.join(waiter);
568
- expect(result).toMatchObject({
569
- failure: { reason: Unauthorized.make({ code: "access_denied" }) },
570
- });
571
- expect(yield* test.inspect(counter.ref)).toMatchObject({
572
- state: { count: 37 },
573
- receipts: 1,
574
- });
575
- expect(yield* counter.Increment(1).pipe(Effect.flip)).toMatchObject({
576
- reason: Unauthorized.make({ code: "access_denied" }),
577
- });
578
- expect(yield* call.pipe(Effect.flip)).toMatchObject({
579
- reason: Unauthorized.make({ code: "access_denied" }),
580
- });
581
- access.allowed = true;
582
- expect(yield* call).toBe(37);
583
- }).pipe(Effect.ensuring(Effect.sync(() => {
584
- access.allowed = true;
585
- })))),
586
- },
587
- {
588
- name: "defines exact expiry boundaries and rejects invalid/future identities",
589
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
590
- const id = "v1.1000.6000.17b3670b-3f17-4a9b-aade-037e1dd1bba8";
591
- yield* checkIdentity(id, 5000, 5999);
592
- for (const now of [6000, 6001])
593
- expect(yield* checkIdentity(id, 5000, now).pipe(Effect.flip)).toMatchObject({
594
- reason: CommandExpired.make({ commandId: id }),
595
- });
596
- expect(yield* checkIdentity(id, 5000, 999).pipe(Effect.flip)).toMatchObject({
597
- reason: InvalidCommandId.make({ commandId: id, code: "future" }),
598
- });
599
- expect(yield* checkIdentity(id.replace("6000", "7000"), 5000, 1000).pipe(Effect.flip)).toMatchObject({
600
- reason: InvalidCommandId.make({
601
- commandId: id.replace("6000", "7000"),
602
- code: "window",
603
- }),
604
- });
605
- const counter = yield* Counter.get("expiry-first");
606
- const before = fixture.executions;
607
- const expired = id.replace("6000", "61000");
608
- expect(yield* counter.Increment(41).pipe(Actor.commandId(expired), Effect.flip)).toMatchObject({
609
- reason: CommandExpired.make({ commandId: expired }),
610
- });
611
- expect(fixture.executions).toBe(before);
612
- })),
613
- },
614
- {
615
- name: "canonicalizes object keys but preserves array order in payload hashes",
616
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
617
- const first = yield* payloadHash('{"value":{"b":2,"a":[3,7]}}');
618
- expect(yield* payloadHash('{"value":{"a":[3,7],"b":2}}')).toBe(first);
619
- expect(yield* payloadHash('{"value":{"a":[7,3],"b":2}}')).not.toBe(first);
620
- })),
621
- },
622
- {
623
- name: "decodes regclass so the migrator can reopen the database",
624
- requiresIndependentConnections: true,
625
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
626
- const sql = yield* SqlClient.SqlClient;
627
- expect(yield* sql `SELECT 128::regclass AS value`).toEqual([{ value: 128 }]);
628
- })),
629
- },
630
- {
631
- name: "recovers a declared failure beforeCommit without persisting dirty state",
632
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
633
- const test = yield* ActorTest;
634
- const counter = yield* Counter.get("failure-beforeCommit");
635
- yield* counter.Increment(19);
636
- const before = fixture.executions;
637
- const call = counter.Reject(53);
638
- yield* test.crashNext("beforeCommit");
639
- expect(yield* call.pipe(Effect.flip)).toEqual(Rejected.make({ amount: 53 }));
640
- expect(fixture.executions - before).toBe(2);
641
- expect(yield* test.inspect(counter.ref)).toMatchObject({
642
- state: { count: 19 },
643
- receipts: 2,
644
- });
645
- yield* counter.Increment(2);
646
- expect(yield* call.pipe(Effect.flip)).toEqual(Rejected.make({ amount: 53 }));
647
- expect(yield* test.inspect(counter.ref)).toMatchObject({
648
- state: { count: 21 },
649
- receipts: 3,
650
- });
651
- })),
652
- },
653
- {
654
- name: "recovers a declared failure afterCommit without persisting dirty state",
655
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
656
- const test = yield* ActorTest;
657
- const counter = yield* Counter.get("failure-afterCommit");
658
- yield* counter.Increment(19);
659
- const before = fixture.executions;
660
- const call = counter.Reject(53);
661
- yield* test.crashNext("afterCommit");
662
- expect(yield* call.pipe(Effect.flip)).toEqual(Rejected.make({ amount: 53 }));
663
- expect(fixture.executions - before).toBe(1);
664
- expect(yield* test.inspect(counter.ref)).toMatchObject({
665
- state: { count: 19 },
666
- receipts: 2,
667
- });
668
- yield* counter.Increment(2);
669
- expect(yield* call.pipe(Effect.flip)).toEqual(Rejected.make({ amount: 53 }));
670
- expect(yield* test.inspect(counter.ref)).toMatchObject({
671
- state: { count: 21 },
672
- receipts: 3,
673
- });
674
- })),
675
- },
676
- {
677
- name: "retries a real generation lock timeout without entering the handler",
678
- requiresIndependentConnections: true,
679
- timeoutMs: 15_000,
680
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
681
- const counter = yield* Counter.get("locked");
682
- const test = yield* ActorTest;
683
- yield* counter.Increment(2);
684
- const connect = environment.connect;
685
- if (connect === undefined)
686
- return yield* Effect.die(new Error("backend lacks independent connections"));
687
- const lock = yield* connect;
688
- yield* lock.query("BEGIN");
689
- yield* lock.query("SELECT generation FROM actor_generations WHERE tenant_id = $1 AND actor_type = $2 AND actor_id = $3 FOR UPDATE", [counter.ref.tenant, counter.ref.actor, counter.ref.id]);
690
- yield* Effect.gen(function* () {
691
- const before = fixture.executions;
692
- const waiter = yield* counter.Increment(59).pipe(Effect.forkChild);
693
- const waitingAttempt = lock.query("SELECT pg_stat_clear_snapshot()").pipe(Effect.andThen(lock.query(`SELECT pid, query_start::text AS query_start
694
- FROM pg_stat_activity
695
- WHERE datname = current_database()
696
- AND pid <> pg_backend_pid()
697
- AND wait_event_type = 'Lock'
698
- AND query LIKE '%actor_generations%FOR UPDATE%'`)), Effect.map((rows) => rows[0]));
699
- const first = yield* waitingAttempt.pipe(Effect.repeat({
700
- while: (attempt) => attempt === undefined,
701
- schedule: Schedule.spaced("10 millis"),
702
- }), Effect.timeout("5 seconds"));
703
- const retry = yield* waitingAttempt.pipe(Effect.repeat({
704
- while: (attempt) => attempt === undefined ||
705
- (attempt.pid === first.pid && attempt.query_start === first.query_start),
706
- schedule: Schedule.spaced("10 millis"),
707
- }), Effect.timeout("5 seconds"));
708
- expect(retry).not.toEqual(first);
709
- expect(fixture.executions).toBe(before);
710
- expect(yield* test.inspect(counter.ref)).toMatchObject({
711
- state: { count: 2 },
712
- receipts: 1,
713
- });
714
- yield* lock.query("COMMIT");
715
- expect(yield* Fiber.join(waiter)).toBe(61);
716
- expect(fixture.executions - before).toBe(1);
717
- expect(yield* test.inspect(counter.ref)).toMatchObject({
718
- state: { count: 61 },
719
- receipts: 2,
720
- });
721
- }).pipe(Effect.ensuring(lock.query("ROLLBACK")));
722
- })),
723
- },
724
- {
725
- name: "completes an in-flight command past expiry but refuses the external outcome",
726
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
727
- const counter = yield* Counter.get("expired-pending");
728
- const test = yield* ActorTest;
729
- const pause = yield* test.pauseNext("beforeHandler");
730
- yield* test.crashNext("beforeCommit");
731
- const now = yield* databaseTime;
732
- const id = `v1.${now - 59_500}.${now + 500}.17b3670b-3f17-4a9b-aade-037e1dd1bba8`;
733
- const waiter = yield* counter
734
- .Increment(67)
735
- .pipe(Actor.commandId(id), Effect.flip, Effect.forkChild);
736
- yield* pause.reached;
737
- yield* Effect.sleep("550 millis");
738
- yield* pause.release;
739
- expect(yield* Fiber.join(waiter)).toMatchObject({
740
- reason: CommandExpired.make({ commandId: id }),
741
- });
742
- expect(yield* test.inspect(counter.ref)).toMatchObject({
743
- state: { count: 67 },
744
- receipts: 1,
745
- });
746
- })),
747
- },
748
- {
749
- name: "rejects an expired identity after receipt pruning and runtime restart",
750
- run: ({ expect, environment }) => Effect.runPromise(Effect.gen(function* () {
751
- const saved = yield* Effect.promise(() => environment.run(Effect.gen(function* () {
752
- const counter = yield* Counter.get("restart");
753
- const now = yield* databaseTime;
754
- const id = `v1.${now - 59_500}.${now + 500}.17b3670b-3f17-4a9b-aade-037e1dd1bba8`;
755
- expect(yield* counter.Increment(43).pipe(Actor.commandId(id))).toBe(43);
756
- const sql = yield* SqlClient.SqlClient;
757
- yield* Effect.sleep("550 millis");
758
- yield* sql `DELETE FROM actor_receipts WHERE tenant_id = ${counter.ref.tenant} AND actor_type = ${counter.ref.actor} AND actor_id = ${counter.ref.id}`;
759
- return { ref: counter.ref, id };
760
- })));
761
- yield* environment.restart;
762
- yield* Effect.promise(() => environment.run(Effect.gen(function* () {
763
- const counter = yield* Counter.get(saved.ref.id).pipe(Actor.tenant(saved.ref.tenant));
764
- expect(yield* counter.Increment(43).pipe(Actor.commandId(saved.id), Effect.flip)).toMatchObject({ reason: CommandExpired.make({ commandId: saved.id }) });
765
- const test = yield* ActorTest;
766
- expect(yield* test.inspect(counter.ref)).toMatchObject({
767
- state: { count: 43 },
768
- receipts: 0,
769
- });
770
- expect(yield* counter.Increment(2)).toBe(45);
771
- })));
772
- })),
773
- },
774
- {
775
- name: "isolates durable state between fresh layer builds",
776
- requiresFreshDatabase: true,
777
- run: ({ expect, environment }) => Effect.runPromise(Effect.gen(function* () {
778
- const tenant = yield* Effect.promise(() => environment.run(Effect.gen(function* () {
779
- const counter = yield* Counter.get("isolated");
780
- expect(yield* counter.Increment(4)).toBe(4);
781
- const test = yield* ActorTest;
782
- return test.tenant;
783
- })));
784
- const database = yield* environment.freshDatabase;
785
- const isolated = yield* Effect.acquireRelease(Effect.sync(() => environment.build({ database })), (runtime) => Effect.promise(() => runtime.dispose()));
786
- yield* Effect.promise(() => isolated.runPromise(Effect.gen(function* () {
787
- const counter = yield* Counter.get("isolated").pipe(Actor.tenant(tenant));
788
- const test = yield* ActorTest;
789
- expect(yield* test.inspect(counter.ref)).toEqual({
790
- generation: undefined,
791
- state: {},
792
- receipts: 0,
793
- events: 0,
794
- outbox: 0,
795
- jobs: 0,
796
- });
797
- expect(yield* counter.Increment(6)).toBe(6);
798
- expect(yield* test.inspect(counter.ref)).toMatchObject({
799
- state: { count: 6 },
800
- receipts: 1,
801
- });
802
- })));
803
- yield* Effect.promise(() => environment.run(Effect.gen(function* () {
804
- const counter = yield* Counter.get("isolated").pipe(Actor.tenant(tenant));
805
- const test = yield* ActorTest;
806
- expect(yield* test.inspect(counter.ref)).toMatchObject({
807
- state: { count: 4 },
808
- receipts: 1,
809
- });
810
- })));
811
- }).pipe(Effect.scoped)),
812
- },
813
- ];