@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,879 +0,0 @@
1
- import { index, pgTable, text } from "drizzle-orm/pg-core";
2
- import { Cause, Context, Crypto, DateTime, Deferred, Effect, Exit, Fiber, Layer, Option, Redacted, Schema, Scope, Stream, } from "effect";
3
- import { FetchHttpClient, Headers as HttpHeaders, HttpClient, HttpClientRequest } from "effect/http";
4
- import { SqlClient } from "effect/sql";
5
- import { retryPoolRefusal } from "../../runtime/database/bounded.js";
6
- import { Actor, Tenant, User } from "../../index.js";
7
- import { InternalActors } from "../../runtime/actors.js";
8
- import { ActorError, InvalidInput, NotCreated, RunnerAtCapacity, SessionEnded, Unauthorized, } from "../../errors/actor.js";
9
- import { ActorTest } from "../actor-test.js";
10
- import { ActorCluster } from "../cluster.js";
11
- import { HttpWatched, serveHttp, tenantOf, httpSuite } from "./http.js";
12
- import { pauseReplay, replayedThrough, withReplica } from "./read-your-writes.js";
13
- import { preparedForRowLevelSecurity } from "./rls.js";
14
- import { Auth } from "../../runtime/index.js";
15
- const Said = Actor.event("Said", { text: Schema.String });
16
- const Aside = Actor.event("Aside", {});
17
- /** The one owned table of `Gauge`, created by `watchLayer` when it is missing. */
18
- export const watchRows = Actor.table(pgTable("conformance_watch_rows", { id: text("id").primaryKey(), label: text("label").notNull() }, (table) => [index("conformance_watch_rows_label").on(table.label)]));
19
- /** What drizzle-kit generates for `watchRows`; `owned.test.ts` checks it stays so. */
20
- export const watchDdl = [
21
- `CREATE TABLE "conformance_watch_rows" (
22
- "routing_key" bigint,
23
- "tenant_id" text,
24
- "actor_id" text,
25
- "id" text,
26
- "label" text NOT NULL,
27
- CONSTRAINT "conformance_watch_rows_pkey" PRIMARY KEY("routing_key","tenant_id","actor_id","id")
28
- );
29
- `,
30
- `ALTER TABLE "conformance_watch_rows" ENABLE ROW LEVEL SECURITY;`,
31
- `CREATE INDEX "conformance_watch_rows_label" ON "conformance_watch_rows" ("routing_key","tenant_id","actor_id","label");`,
32
- `CREATE POLICY "durable_tenant" ON "conformance_watch_rows" AS PERMISSIVE FOR ALL TO public USING (tenant_id = current_setting('durable.tenant', true)) WITH CHECK (tenant_id = current_setting('durable.tenant', true));`,
33
- ];
34
- const stash = Actor.blob("watch-stash");
35
- export const watchFixture = () => ({ runs: new Map(), gate: Effect.void });
36
- const Bump = Actor.command("Bump", { payload: Schema.Finite });
37
- const Say = Actor.command("Say", { payload: Schema.String });
38
- const Glance = Actor.command("Glance");
39
- const Label = Actor.command("Label", { payload: Schema.String });
40
- const Store = Actor.command("Store", { payload: Schema.String });
41
- const Idle = Actor.command("Idle");
42
- const Total = Actor.query("Total", { success: Schema.Finite, watch: true });
43
- const Gated = Actor.query("Gated", { success: Schema.Finite, watch: true });
44
- const Sayings = Actor.query("Sayings", { success: Schema.Array(Schema.String), watch: true });
45
- const Retained = Actor.query("Retained", { success: Schema.Int, watch: true });
46
- const Labels = Actor.query("Labels", { success: Schema.Array(Schema.String), watch: true });
47
- const Stored = Actor.query("Stored", { success: Schema.String, watch: true });
48
- const Asides = Actor.query("Asides", { success: Schema.Int, watch: true });
49
- const Grouped = Actor.query("Grouped", { success: Schema.Int, watch: true });
50
- const Plain = Actor.query("Plain", { success: Schema.Finite });
51
- const Flood = Actor.stream("Flood", { success: Schema.Finite });
52
- const gaugeState = Actor.state({
53
- count: Schema.Finite.pipe(Schema.withDecodingDefault(Effect.succeed(0))),
54
- });
55
- /** Every kind of read a watch records, over state, one event class, one table, and one blob. */
56
- const Gauge = Actor.make("WatchGauge", {
57
- key: Schema.String,
58
- state: gaugeState,
59
- events: [Said, Aside],
60
- tables: [watchRows],
61
- blobs: [stash],
62
- api: {
63
- Bump,
64
- Say,
65
- Glance,
66
- Label,
67
- Store,
68
- Idle,
69
- Total,
70
- Gated,
71
- Sayings,
72
- Retained,
73
- Labels,
74
- Stored,
75
- Asides,
76
- Grouped,
77
- Plain,
78
- Flood,
79
- },
80
- policy: {
81
- hibernateAfter: "1 second",
82
- keepEvents: "5 seconds",
83
- watch: { minInterval: "10 millis", reconcileEvery: "5 seconds" },
84
- },
85
- });
86
- const texts = new TextEncoder();
87
- const decoded = new TextDecoder();
88
- const GaugeLive = Gauge.toLayer(Effect.succeed({
89
- Bump: Effect.fnUntraced(function* (by) {
90
- const turn = yield* Gauge.Turn;
91
- yield* turn.state.set({ count: turn.state.count + by });
92
- }),
93
- Say: Effect.fnUntraced(function* (text) {
94
- yield* (yield* Gauge.Turn).emit(Said.make({ text }));
95
- }),
96
- Glance: Effect.fnUntraced(function* () {
97
- yield* (yield* Gauge.Turn).emit(Aside.make({}));
98
- }),
99
- Label: Effect.fnUntraced(function* (label) {
100
- yield* (yield* Gauge.Turn).rows(watchRows).insert({ id: label, label });
101
- }),
102
- Store: Effect.fnUntraced(function* (text) {
103
- yield* (yield* Gauge.Turn).blob(stash).set("box", texts.encode(text));
104
- }),
105
- Idle: () => Effect.void,
106
- Flood: () => Stream.iterate(0, (value) => value + 1),
107
- }));
108
- const GaugeReads = (fixture) => {
109
- const ran = (query) => Effect.sync(() => fixture.runs.set(query, (fixture.runs.get(query) ?? 0) + 1));
110
- return Gauge.toQueryLayer(Effect.succeed({
111
- Total: Effect.fnUntraced(function* () {
112
- yield* ran("Total");
113
- return (yield* Gauge.Read).state.count;
114
- }),
115
- Gated: Effect.fnUntraced(function* () {
116
- yield* ran("Gated");
117
- const count = (yield* Gauge.Read).state.count;
118
- yield* Effect.suspend(() => fixture.gate);
119
- return count;
120
- }),
121
- Sayings: Effect.fnUntraced(function* () {
122
- yield* ran("Sayings");
123
- const said = yield* (yield* Gauge.Read).events(Said).pipe(Effect.orDie);
124
- return said.map(({ event }) => event.text);
125
- }),
126
- Retained: Effect.fnUntraced(function* () {
127
- yield* ran("Retained");
128
- return yield* (yield* Gauge.Read).events(Said).pipe(Effect.map((said) => said.length), Effect.orElseSucceed(() => -1));
129
- }),
130
- Labels: Effect.fnUntraced(function* () {
131
- yield* ran("Labels");
132
- const found = yield* (yield* Gauge.Read).rows(watchRows).all({ orderBy: { id: "asc" } });
133
- return found.map(({ label }) => label);
134
- }),
135
- Stored: Effect.fnUntraced(function* () {
136
- yield* ran("Stored");
137
- const found = yield* (yield* Gauge.Read).blob(stash).get("box");
138
- return Option.match(found, { onNone: () => "", onSome: (bytes) => decoded.decode(bytes) });
139
- }),
140
- Asides: Effect.fnUntraced(function* () {
141
- yield* ran("Asides");
142
- return (yield* (yield* Gauge.Read).events(Aside).pipe(Effect.orDie)).length;
143
- }),
144
- Grouped: Effect.fnUntraced(function* () {
145
- yield* ran("Grouped");
146
- const found = yield* (yield* Gauge.Read).group((db) => db.select({ id: watchRows.id }).from(watchRows));
147
- return found.length;
148
- }),
149
- Plain: Effect.fnUntraced(function* () {
150
- return (yield* Gauge.Read).state.count;
151
- }),
152
- }));
153
- };
154
- const Mirror = Actor.make("WatchMirror", {
155
- key: Schema.String,
156
- state: gaugeState,
157
- api: { Bump, Total },
158
- policy: {
159
- reauthorizeEvery: "2 seconds",
160
- watch: { minInterval: "10 millis", reconcileEvery: "5 seconds" },
161
- },
162
- });
163
- /** A state-only actor, so a replica can answer its watch and a cluster builds it without a database table. */
164
- export const mirrorLayer = Layer.mergeAll(Mirror.toLayer(Effect.succeed({
165
- Bump: Effect.fnUntraced(function* (by) {
166
- const turn = yield* Mirror.Turn;
167
- yield* turn.state.set({ count: turn.state.count + by });
168
- }),
169
- })), Mirror.toQueryLayer(Effect.succeed({
170
- Total: Effect.fnUntraced(function* () {
171
- return (yield* Mirror.Read).state.count;
172
- }),
173
- })));
174
- const Capped = Actor.make("WatchCapped", {
175
- key: Schema.String,
176
- state: gaugeState,
177
- api: { Bump, Total },
178
- policy: { watch: { maxPerActor: 3 } },
179
- });
180
- const cappedLayer = Layer.mergeAll(Capped.toLayer(Effect.succeed({
181
- Bump: Effect.fnUntraced(function* (by) {
182
- const turn = yield* Capped.Turn;
183
- yield* turn.state.set({ count: turn.state.count + by });
184
- }),
185
- })), Capped.toQueryLayer(Effect.succeed({
186
- Total: Effect.fnUntraced(function* () {
187
- return (yield* Capped.Read).state.count;
188
- }),
189
- })));
190
- /** Applies the drizzle-kit DDL of `watchRows` once, then registers the actor types of this file. */
191
- export const watchLayer = (fixture) => Layer.unwrap(Effect.gen(function* () {
192
- const sql = yield* SqlClient.SqlClient;
193
- const existing = yield* retryPoolRefusal(sql `
194
- SELECT relname FROM pg_class WHERE relname = 'conformance_watch_rows'`);
195
- if (existing.length === 0)
196
- for (const statement of watchDdl)
197
- yield* retryPoolRefusal(sql.unsafe(statement));
198
- return Layer.mergeAll(GaugeLive, GaugeReads(fixture), mirrorLayer, cappedLayer, unsafeLayer);
199
- }).pipe(Effect.orDie));
200
- const WAIT = "20 seconds";
201
- /**
202
- * A watch read by a child fiber: `seen` fills as results arrive, `until(n)`
203
- * waits for the n-th, and `ended` joins to how the watch ended.
204
- */
205
- const observe = (stream) => Effect.gen(function* () {
206
- const seen = [];
207
- const ended = yield* stream.pipe(Stream.runForEach((value) => Effect.sync(() => seen.push(value))), Effect.exit, Effect.forkChild);
208
- const until = (count) => Effect.sleep("25 millis").pipe(Effect.repeat({ until: () => seen.length >= count }), Effect.timeoutOrElse({
209
- duration: WAIT,
210
- orElse: () => Effect.die(new Error(`Expected ${count} results, saw ${JSON.stringify(seen)}`)),
211
- }), Effect.asVoid);
212
- const failure = Fiber.join(ended).pipe(Effect.timeoutOrElse({
213
- duration: WAIT,
214
- orElse: () => Effect.die(new Error("The watch did not end")),
215
- }), Effect.map((exit) => Exit.isFailure(exit) ? Option.getOrUndefined(Cause.findErrorOption(exit.cause)) : undefined));
216
- return { seen, until, ended, failure };
217
- });
218
- const reasonOf = (error) => error?.reason;
219
- /** Long enough for a rerun the watch should not make to have started. */
220
- const QUIET = "400 millis";
221
- const runsOf = (fixture, query) => fixture.runs.get(query) ?? 0;
222
- const EXPIRATION_SECONDS = 3;
223
- const withCluster = (environment, fixture, runners, body, holdersOnly) => environment.run(Effect.gen(function* () {
224
- const database = yield* environment.freshDatabase;
225
- const context = yield* Layer.build(ActorTest.cluster({
226
- database,
227
- runners,
228
- shardLockExpiration: `${EXPIRATION_SECONDS} seconds`,
229
- actors: watchLayer(fixture),
230
- as: User.make({ subject: "alice" }),
231
- authorize: () => Effect.succeed(true),
232
- holdersOnly,
233
- }));
234
- return yield* body.pipe(Effect.scoped, Effect.provideContext(context));
235
- }));
236
- /** An actor id that `runner` owns, probed through runner 0. */
237
- const ownedBy = (runner, prefix) => Effect.gen(function* () {
238
- const cluster = yield* ActorCluster;
239
- for (let index = 0; index < 200; index++) {
240
- const candidate = (yield* cluster.on(0)(Mirror.get(`${prefix}-${index}`))).ref;
241
- if ((yield* cluster.owner(candidate)) === runner)
242
- return candidate;
243
- }
244
- return yield* Effect.die(new Error(`Runner ${runner} owns no probed actor`));
245
- });
246
- const Unsafe = Actor.make("WatchUnsafe", {
247
- key: Schema.String,
248
- state: gaugeState,
249
- api: { Bump, Total },
250
- });
251
- class Outside extends Context.Service()("@rikalabs/akter/testing/conformance/watch/Outside") {
252
- }
253
- /** A watched handler that needs a service the types forbid, as code that got around them would. */
254
- const unsafeLayer = Layer.mergeAll(Unsafe.toLayer(Effect.succeed({
255
- Bump: Effect.fnUntraced(function* (by) {
256
- const turn = yield* Unsafe.Turn;
257
- yield* turn.state.set({ count: turn.state.count + by });
258
- }),
259
- })), Unsafe.toQueryLayer(Effect.succeed({
260
- Total: (() => Effect.gen(function* () {
261
- return (yield* Unsafe.Read).state.count + (yield* Outside).value;
262
- })),
263
- })).pipe(Layer.provide(Layer.succeed(Outside, { value: 1 }))));
264
- const decodeWireReason = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Struct({
265
- reason: Schema.Struct({ _tag: Schema.String, code: Schema.optionalKey(Schema.String) }),
266
- })));
267
- /** The reason tag and code a served error body or `end` message carries. */
268
- const wireReason = (text) => decodeWireReason(text).pipe(Effect.orDie, Effect.map(({ reason }) => ({ tag: reason._tag, code: reason.code })));
269
- /**
270
- * A served watch read over HTTP, as the Promise client reads one: SSE messages
271
- * parsed from the body, comments skipped, closed with the scope.
272
- */
273
- const watchOver = (url, init) => Effect.gen(function* () {
274
- const client = Context.get(yield* Layer.build(FetchHttpClient.layer), HttpClient.HttpClient);
275
- const response = yield* client
276
- .execute(HttpClientRequest.post(url, {
277
- headers: { authorization: `Bearer ${init.token}`, ...init.headers },
278
- }))
279
- .pipe(Effect.orDie);
280
- const messages = [];
281
- if (response.status !== 200)
282
- return { status: response.status, messages, until: () => Effect.void };
283
- let buffer = "";
284
- yield* response.stream.pipe(Stream.decodeText, Stream.runForEach((chunk) => Effect.sync(() => {
285
- buffer += chunk;
286
- let end = buffer.indexOf("\n\n");
287
- while (end !== -1) {
288
- const block = buffer.slice(0, end);
289
- buffer = buffer.slice(end + 2);
290
- end = buffer.indexOf("\n\n");
291
- const fields = new Map();
292
- for (const line of block.split("\n"))
293
- if (!line.startsWith(":")) {
294
- const colon = line.indexOf(":");
295
- fields.set(line.slice(0, colon), line.slice(colon + 2));
296
- }
297
- if (fields.has("data"))
298
- messages.push({
299
- id: fields.get("id"),
300
- event: fields.get("event"),
301
- data: fields.get("data"),
302
- });
303
- }
304
- })), Effect.ignore, Effect.forkScoped);
305
- const until = (count) => Effect.sleep("25 millis").pipe(Effect.repeat({ until: () => messages.length >= count }), Effect.timeoutOrElse({
306
- duration: WAIT,
307
- orElse: () => Effect.die(new Error(`Expected ${count} messages, saw ${JSON.stringify(messages)}`)),
308
- }), Effect.asVoid);
309
- return { status: 200, messages, until };
310
- });
311
- const baseFetch = globalThis.fetch.bind(globalThis);
312
- const VERSION = /^(0|[1-9]\d*)$/;
313
- const post = (server, path, token) => Effect.flatMap(server.mint(), (key) => server.send(path, { token, key, body: { text: "a" } }));
314
- /** `tenant:subject:expiresAtMs`, so a credential can expire while a watch is open. */
315
- const expiring = Auth.make((request) => Effect.gen(function* () {
316
- const header = Option.getOrUndefined(HttpHeaders.get(request.headers, "authorization")) ?? "";
317
- const match = /^Bearer ([^:]+):([^:]+):(\d+)$/.exec(header);
318
- if (match === null)
319
- return yield* Unauthorized.make({ code: "invalid_credentials" });
320
- const authenticated = {
321
- tenant: match[1],
322
- caller: User.make({ subject: match[2] }),
323
- expiresAt: DateTime.makeUnsafe(Number(match[3])),
324
- };
325
- return authenticated;
326
- }));
327
- const servedCases = [
328
- {
329
- name: "serves a watch as server-sent events: the current result first, then one per change, each after the first carrying the version its rerun waited for",
330
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
331
- const server = yield* serveHttp();
332
- const token = `${yield* tenantOf}:alice`;
333
- const base = `/actors/HttpWatched/served-events`;
334
- const first = yield* post(server, `${base}/Post`, token);
335
- const watch = yield* watchOver(`${server.url}${base}/Level/watch`, { token });
336
- yield* watch.until(1);
337
- expect(watch.messages[0]).toEqual({ id: undefined, event: "result", data: "1" });
338
- const second = yield* post(server, `${base}/Post`, token);
339
- yield* watch.until(2);
340
- expect(watch.messages[1]).toEqual({
341
- id: second.headers.get("durable-version"),
342
- event: "result",
343
- data: "2",
344
- });
345
- expect(VERSION.test(watch.messages[1].id)).toBe(true);
346
- expect(BigInt(watch.messages[1].id) > BigInt(first.headers.get("durable-version"))).toBe(true);
347
- const resumed = yield* watchOver(`${server.url}${base}/Level/watch`, {
348
- token,
349
- headers: { "durable-min-version": second.headers.get("durable-version") },
350
- });
351
- yield* resumed.until(1);
352
- expect(resumed.messages[0]).toEqual({
353
- id: second.headers.get("durable-version"),
354
- event: "result",
355
- data: "2",
356
- });
357
- })),
358
- },
359
- {
360
- name: "refuses a watch of a query without watch with 400 not_watchable, of an actor no command created with 404 NotCreated, and a malformed durable-min-version",
361
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
362
- const server = yield* serveHttp();
363
- const token = `${yield* tenantOf}:alice`;
364
- yield* post(server, `/actors/HttpWatched/served-refused/Post`, token);
365
- const plain = yield* server.send("/actors/HttpWatched/served-refused/Count/watch", {
366
- token,
367
- });
368
- expect(plain.status).toBe(400);
369
- expect(yield* wireReason(plain.text)).toEqual({
370
- tag: "InvalidInput",
371
- code: "not_watchable",
372
- });
373
- const missing = yield* server.send("/actors/HttpWatched/served-missing/Level/watch", {
374
- token,
375
- });
376
- expect(missing.status).toBe(404);
377
- expect((yield* wireReason(missing.text)).tag).toBe("NotCreated");
378
- const malformed = yield* server.send("/actors/HttpWatched/served-refused/Level/watch", {
379
- token,
380
- headers: { "durable-min-version": "01" },
381
- });
382
- expect(malformed.status).toBe(400);
383
- const anonymous = yield* server.send("/actors/HttpWatched/served-refused/Level/watch", {});
384
- expect(anonymous.status).toBe(401);
385
- })),
386
- },
387
- {
388
- name: "denies a watch at open with 403, and ends a running one with access_denied within reauthorizeEvery after revocation",
389
- timeoutMs: 60_000,
390
- run: ({ expect, environment, access }) => environment.run(Effect.gen(function* () {
391
- const test = yield* ActorTest;
392
- const server = yield* serveHttp();
393
- const token = `${yield* tenantOf}:alice`;
394
- const base = `/actors/HttpWatched/served-revoked`;
395
- yield* post(server, `${base}/Post`, token);
396
- access.denied.add("Level");
397
- const denied = yield* server
398
- .send(`${base}/Level/watch`, { token })
399
- .pipe(Effect.ensuring(Effect.sync(() => access.denied.delete("Level"))));
400
- expect(denied.status).toBe(403);
401
- expect(yield* wireReason(denied.text)).toEqual({
402
- tag: "Unauthorized",
403
- code: "access_denied",
404
- });
405
- const watch = yield* watchOver(`${server.url}${base}/Level/watch`, { token });
406
- yield* watch.until(1);
407
- access.denied.add("Level");
408
- yield* test.advance("55 seconds");
409
- yield* watch
410
- .until(2)
411
- .pipe(Effect.ensuring(Effect.sync(() => access.denied.delete("Level"))));
412
- expect(watch.messages[1]?.event).toBe("end");
413
- expect(yield* wireReason(watch.messages[1].data)).toEqual({
414
- tag: "Unauthorized",
415
- code: "access_denied",
416
- });
417
- })),
418
- },
419
- {
420
- name: "ends a watch at its credential's expiry with Unauthorized expired, and sends no result after it",
421
- timeoutMs: 60_000,
422
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
423
- const server = yield* serveHttp({ auth: expiring });
424
- const tenant = yield* tenantOf;
425
- const base = `/actors/HttpWatched/served-expiry`;
426
- const test = yield* ActorTest;
427
- const now = DateTime.toEpochMillis(yield* test.now);
428
- const ttl = now + 60_000;
429
- yield* post(server, `${base}/Post`, `${tenant}:alice:${ttl}`);
430
- const soon = `${tenant}:alice:${DateTime.toEpochMillis(yield* test.now) + 30_000}`;
431
- const watch = yield* watchOver(`${server.url}${base}/Level/watch`, { token: soon });
432
- yield* watch.until(1);
433
- expect(watch.messages[0]?.event).toBe("result");
434
- yield* test.advance("31 seconds");
435
- yield* watch.until(2);
436
- expect(watch.messages[1]?.event).toBe("end");
437
- expect(yield* wireReason(watch.messages[1].data)).toEqual({
438
- tag: "Unauthorized",
439
- code: "expired",
440
- });
441
- yield* post(server, `${base}/Post`, `${tenant}:alice:${ttl}`);
442
- yield* Effect.sleep(QUIET);
443
- expect(watch.messages.length).toBe(2);
444
- })),
445
- },
446
- {
447
- name: "the Promise client watches a query, reopens a dropped connection with the greatest version it was sent, and ends with a typed error",
448
- timeoutMs: 60_000,
449
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
450
- const server = yield* serveHttp();
451
- const tenant = yield* tenantOf;
452
- const token = `${tenant}:alice`;
453
- const base = `/actors/HttpWatched/served-client`;
454
- yield* post(server, `${base}/Post`, token);
455
- const opened = [];
456
- const cutting = (input, init) => baseFetch(input, init).then((response) => {
457
- opened.push(new Headers(init?.headers));
458
- if (opened.length > 1 || response.body === null)
459
- return response;
460
- const source = response.body.getReader();
461
- let results = 0;
462
- return new Response(new ReadableStream({
463
- pull: (controller) => source.read().then((chunk) => {
464
- if (chunk.done)
465
- return controller.close();
466
- controller.enqueue(chunk.value);
467
- results +=
468
- new TextDecoder().decode(chunk.value).split("event: result").length - 1;
469
- if (results < 2)
470
- return undefined;
471
- controller.close();
472
- return source.cancel();
473
- }),
474
- }), { status: response.status, headers: response.headers });
475
- });
476
- const room = HttpWatched.client({
477
- baseUrl: server.url,
478
- headers: { authorization: `Bearer ${token}` },
479
- fetch: cutting,
480
- }).get("served-client");
481
- const iterator = room.Level.watch()[Symbol.asyncIterator]();
482
- const next = () => Effect.promise(() => iterator.next());
483
- expect((yield* next()).value).toBe(1);
484
- const second = yield* post(server, `${base}/Post`, token);
485
- expect((yield* next()).value).toBe(2);
486
- yield* post(server, `${base}/Post`, token);
487
- expect((yield* next()).value).toBe(3);
488
- expect(opened.length).toBe(2);
489
- expect(opened[0].get("durable-min-version")).toBe(null);
490
- expect(opened[1].get("durable-min-version")).toBe(second.headers.get("durable-version"));
491
- yield* Effect.promise(() => Promise.resolve(iterator.return?.()));
492
- })),
493
- },
494
- ];
495
- export const watchConformance = [
496
- ...servedCases,
497
- {
498
- name: "sends the current result first, then a rerun after a turn that writes the state the query read",
499
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
500
- const gauge = yield* Gauge.get("watch-state");
501
- yield* gauge.Bump(2);
502
- const watch = yield* observe(gauge.Total.watch());
503
- yield* watch.until(1);
504
- expect(watch.seen).toEqual([2]);
505
- yield* gauge.Bump(3);
506
- yield* watch.until(2);
507
- expect(watch.seen).toEqual([2, 5]);
508
- expect(runsOf(fixture, "Total")).toBe(2);
509
- })),
510
- },
511
- {
512
- name: "sends a rerun after a turn that emits an event class the query read",
513
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
514
- const gauge = yield* Gauge.get("watch-event");
515
- yield* gauge.Say("one");
516
- const watch = yield* observe(gauge.Sayings.watch());
517
- yield* watch.until(1);
518
- yield* gauge.Say("two");
519
- yield* watch.until(2);
520
- expect(watch.seen).toEqual([["one"], ["one", "two"]]);
521
- })),
522
- },
523
- {
524
- name: "sends a rerun after a turn that writes a table the query read",
525
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
526
- const gauge = yield* Gauge.get("watch-table");
527
- yield* gauge.Label("a");
528
- const watch = yield* observe(gauge.Labels.watch());
529
- yield* watch.until(1);
530
- yield* gauge.Label("b");
531
- yield* watch.until(2);
532
- expect(watch.seen).toEqual([["a"], ["a", "b"]]);
533
- })),
534
- },
535
- {
536
- name: "sends a rerun after a turn that writes a blob the query read",
537
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
538
- const gauge = yield* Gauge.get("watch-blob");
539
- yield* gauge.Store("first");
540
- const watch = yield* observe(gauge.Stored.watch());
541
- yield* watch.until(1);
542
- yield* gauge.Store("second");
543
- yield* watch.until(2);
544
- expect(watch.seen).toEqual(["first", "second"]);
545
- })),
546
- },
547
- {
548
- name: "does not send a result when the rerun is identical, and skips reruns for a turn that wrote nothing the query read",
549
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
550
- const gauge = yield* Gauge.get("watch-quiet");
551
- yield* gauge.Bump(1);
552
- const totals = yield* observe(gauge.Total.watch());
553
- const asides = yield* observe(gauge.Asides.watch());
554
- yield* totals.until(1);
555
- yield* asides.until(1);
556
- const totalRuns = runsOf(fixture, "Total");
557
- const asideRuns = runsOf(fixture, "Asides");
558
- yield* gauge.Bump(0);
559
- yield* gauge.Say("unread");
560
- yield* gauge.Label("unread");
561
- yield* gauge.Store("unread");
562
- yield* gauge.Idle();
563
- yield* Effect.sleep(QUIET);
564
- expect(runsOf(fixture, "Total")).toBe(totalRuns + 1);
565
- expect(totals.seen).toEqual([1]);
566
- expect(runsOf(fixture, "Asides")).toBe(asideRuns);
567
- expect(asides.seen).toEqual([0]);
568
- yield* gauge.Glance();
569
- yield* asides.until(2);
570
- expect(asides.seen).toEqual([0, 1]);
571
- })),
572
- },
573
- {
574
- name: "coalesces commits that land during a rerun into one further rerun, and never sends a result reflecting an older version than an earlier one",
575
- requiresIndependentConnections: true,
576
- timeoutMs: 60_000,
577
- run: ({ expect, environment, fixture }) => environment.run(Effect.gen(function* () {
578
- const gauge = yield* Gauge.get("watch-coalesce");
579
- yield* gauge.Bump(0);
580
- const watch = yield* observe(gauge.Gated.watch());
581
- yield* watch.until(1);
582
- yield* gauge.Bump(1);
583
- yield* watch.until(2);
584
- const before = runsOf(fixture, "Gated");
585
- const release = yield* Deferred.make();
586
- fixture.gate = Deferred.await(release);
587
- yield* gauge.Bump(1);
588
- yield* Effect.sleep(QUIET);
589
- yield* gauge.Bump(1);
590
- yield* gauge.Bump(1);
591
- yield* gauge.Bump(1);
592
- yield* Effect.sleep(QUIET);
593
- expect(runsOf(fixture, "Gated")).toBe(before + 1);
594
- fixture.gate = Effect.void;
595
- yield* Deferred.succeed(release, undefined);
596
- yield* watch.until(4);
597
- yield* Effect.sleep(QUIET);
598
- expect(runsOf(fixture, "Gated")).toBe(before + 2);
599
- expect(watch.seen).toEqual([0, 1, 2, 5]);
600
- })),
601
- },
602
- {
603
- name: "recovers a dropped Committed frame on the reconcile rerun",
604
- timeoutMs: 60_000,
605
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
606
- const test = yield* ActorTest;
607
- const gauge = yield* Gauge.get("watch-dropped");
608
- yield* gauge.Bump(1);
609
- const watch = yield* observe(gauge.Total.watch());
610
- yield* watch.until(1);
611
- yield* test.dropCommitted(true);
612
- yield* gauge.Bump(1);
613
- yield* Effect.sleep(QUIET);
614
- expect(watch.seen).toEqual([1]);
615
- yield* test.dropCommitted(false);
616
- yield* watch.until(2);
617
- expect(watch.seen).toEqual([1, 2]);
618
- })),
619
- },
620
- {
621
- name: "recovers a retention prune of a read event class the same way",
622
- timeoutMs: 60_000,
623
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
624
- const test = yield* ActorTest;
625
- const gauge = yield* Gauge.get("watch-pruned");
626
- yield* gauge.Say("old");
627
- const watch = yield* observe(gauge.Retained.watch());
628
- yield* watch.until(1);
629
- expect(watch.seen).toEqual([1]);
630
- yield* test.advance("6 seconds");
631
- yield* test.cleanup;
632
- yield* watch.until(2);
633
- expect(watch.seen).toEqual([1, -1]);
634
- })),
635
- },
636
- {
637
- name: "ends a running watch that starts reading the placement group with not_watchable, as a defect in process",
638
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
639
- const gauge = yield* Gauge.get("watch-group");
640
- yield* gauge.Bump(1);
641
- const watch = yield* observe(gauge.Grouped.watch());
642
- const exit = yield* Fiber.join(watch.ended);
643
- expect(watch.seen).toEqual([]);
644
- expect(Exit.isFailure(exit) && Cause.hasDies(exit.cause)).toBe(true);
645
- const thrown = Exit.isFailure(exit) ? Cause.squash(exit.cause) : undefined;
646
- const reason = Schema.is(ActorError)(thrown) ? thrown.reason : undefined;
647
- expect(Schema.is(InvalidInput)(reason) && reason.code).toBe("not_watchable");
648
- })),
649
- },
650
- {
651
- name: "refuses a watch of an actor no command has created with NotCreated, and creates none",
652
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
653
- const test = yield* ActorTest;
654
- const gauge = yield* Gauge.get("watch-never-created");
655
- const ended = yield* gauge.Total.watch().pipe(Stream.runDrain, Effect.flip);
656
- expect(reasonOf(ended)).toEqual(NotCreated.make({}));
657
- expect((yield* test.inspect(gauge.ref)).generation).toBe(undefined);
658
- })),
659
- },
660
- {
661
- name: "denies a watch at open, ends it within reauthorizeEvery after revocation, and sends no result after the bound",
662
- timeoutMs: 60_000,
663
- run: ({ expect, environment, access }) => environment.run(Effect.gen(function* () {
664
- const test = yield* ActorTest;
665
- const gauge = yield* Mirror.get("watch-revoked");
666
- yield* gauge.Bump(1);
667
- access.denied.add("Total");
668
- const refused = yield* gauge.Total.watch().pipe(Stream.runDrain, Effect.flip, Effect.ensuring(Effect.sync(() => access.denied.delete("Total"))));
669
- expect(reasonOf(refused)).toEqual(Unauthorized.make({ code: "access_denied" }));
670
- const watch = yield* observe(gauge.Total.watch());
671
- yield* watch.until(1);
672
- access.denied.add("Total");
673
- yield* test.advance("1500 millis");
674
- const revoked = yield* watch.failure.pipe(Effect.ensuring(Effect.sync(() => access.denied.delete("Total"))));
675
- expect(reasonOf(revoked)).toEqual(Unauthorized.make({ code: "access_denied" }));
676
- const late = yield* observe(gauge.Total.watch());
677
- yield* late.until(1);
678
- yield* test.advance("3 seconds");
679
- yield* gauge.Bump(1);
680
- const lapsed = yield* late.failure;
681
- expect(reasonOf(lapsed)).toEqual(Unauthorized.make({ code: "reauthorization_unavailable" }));
682
- expect(late.seen).toEqual([1]);
683
- })),
684
- },
685
- {
686
- name: "keeps tenants apart: another tenant's caller cannot reach the actor's watch",
687
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
688
- const mine = yield* Mirror.get("watch-tenants");
689
- yield* mine.Bump(4);
690
- const other = yield* Mirror.get("watch-tenants").pipe(Effect.provideService(Tenant, "watch-elsewhere"));
691
- const refused = yield* other.Total.watch().pipe(Stream.runDrain, Effect.flip);
692
- expect(reasonOf(refused)).toEqual(NotCreated.make({}));
693
- const watch = yield* observe(mine.Total.watch());
694
- yield* watch.until(1);
695
- expect(watch.seen).toEqual([4]);
696
- })),
697
- },
698
- {
699
- name: "an idle watch does not keep the actor resident, survives its hibernation, and does not count against the stream cap",
700
- timeoutMs: 60_000,
701
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
702
- const test = yield* ActorTest;
703
- const gauge = yield* Gauge.get("watch-idle");
704
- yield* gauge.Bump(1);
705
- const watch = yield* observe(gauge.Total.watch());
706
- yield* watch.until(1);
707
- const generation = BigInt((yield* test.inspect(gauge.ref)).generation);
708
- yield* test.hibernate(gauge.ref);
709
- yield* Effect.sleep(QUIET);
710
- yield* gauge.Bump(1);
711
- yield* watch.until(2);
712
- expect(BigInt((yield* test.inspect(gauge.ref)).generation) > generation).toBe(true);
713
- expect(watch.seen).toEqual([1, 2]);
714
- const floods = yield* Effect.forEach(Array.from({ length: 256 }), () => Effect.flatMap(Stream.toPull(gauge.Flood()), (pull) => pull), { concurrency: "unbounded" });
715
- expect(floods.length).toBe(256);
716
- })),
717
- },
718
- {
719
- name: "a further watch past the per-actor cap answers RunnerAtCapacity",
720
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
721
- const capped = yield* Capped.get("watch-capped");
722
- yield* capped.Bump(1);
723
- const open = yield* Effect.forEach(Array.from({ length: 3 }), () => observe(capped.Total.watch()));
724
- yield* Effect.forEach(open, (watch) => watch.until(1), { discard: true });
725
- const refused = yield* capped.Total.watch().pipe(Stream.runDrain, Effect.flip);
726
- expect(reasonOf(refused)).toEqual(RunnerAtCapacity.make({}));
727
- yield* Fiber.interrupt(open[0].ended);
728
- yield* Effect.sleep("300 millis");
729
- const replaced = yield* observe(capped.Total.watch());
730
- yield* replaced.until(1);
731
- expect(replaced.seen).toEqual([1]);
732
- })),
733
- },
734
- {
735
- name: "a slow consumer receives the newest result and the watch stays open",
736
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
737
- const gauge = yield* Gauge.get("watch-slow");
738
- yield* gauge.Bump(0);
739
- const pull = yield* Stream.toPull(gauge.Total.watch());
740
- const first = yield* pull.pipe(Effect.timeout(WAIT));
741
- expect([...first]).toEqual([0]);
742
- for (const by of [1, 1, 1, 1, 1, 1]) {
743
- yield* gauge.Bump(by);
744
- yield* Effect.sleep("100 millis");
745
- }
746
- yield* Effect.sleep(QUIET);
747
- const newest = yield* pull.pipe(Effect.timeout(WAIT));
748
- expect([...newest]).toEqual([6]);
749
- yield* gauge.Bump(1);
750
- const following = yield* pull.pipe(Effect.timeout(WAIT));
751
- expect([...following]).toEqual([7]);
752
- })),
753
- },
754
- {
755
- name: "a handler that needs a service outside X.Read fails on its first rerun instead of running unwatched",
756
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
757
- const unsafe = yield* Unsafe.get("watch-unsafe");
758
- yield* unsafe.Bump(1);
759
- const ended = yield* unsafe.Total.watch().pipe(Stream.runDrain, Effect.flip);
760
- const reason = reasonOf(ended);
761
- expect(Schema.is(SessionEnded)(reason) && reason.cause).toBe("Defect");
762
- })),
763
- },
764
- {
765
- name: "rerun waits for the frame's version: a lagging replica is bypassed and the primary answers",
766
- requiresReplica: true,
767
- timeoutMs: 60_000,
768
- run: ({ expect, environment }) => {
769
- const replica = environment.replica;
770
- return withReplica(environment, replica.database, Effect.gen(function* () {
771
- const control = yield* replica.connect;
772
- const internal = yield* InternalActors;
773
- const mirror = yield* Mirror.get("watch-replica");
774
- yield* mirror.Bump(1).pipe(Effect.orDie);
775
- yield* replayedThrough(control, internal.observedVersion());
776
- const watch = yield* observe(mirror.Total.watch());
777
- yield* watch.until(1);
778
- expect(watch.seen).toEqual([1]);
779
- yield* pauseReplay(control);
780
- yield* mirror.Bump(1).pipe(Effect.orDie);
781
- yield* watch.until(2);
782
- expect(watch.seen).toEqual([1, 2]);
783
- }));
784
- },
785
- },
786
- {
787
- name: "with row-level security on, reruns run as the role bound to the tenant on whichever server answers",
788
- requiresFreshDatabase: true,
789
- requiresIndependentConnections: true,
790
- timeoutMs: 90_000,
791
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
792
- const { target, role } = yield* preparedForRowLevelSecurity(environment);
793
- if (!Redacted.isRedacted(target))
794
- return yield* Effect.die(new Error("The row-level security case needs Postgres"));
795
- const crypto = yield* Crypto.Crypto;
796
- const context = yield* Layer.build(ActorTest.cluster({
797
- database: target,
798
- runners: 2,
799
- shardLockExpiration: `${EXPIRATION_SECONDS} seconds`,
800
- actors: mirrorLayer,
801
- as: User.make({ subject: "alice" }),
802
- authorize: () => Effect.succeed(true),
803
- rowLevelSecurity: { role },
804
- }).pipe(Layer.provide(Layer.succeed(Crypto.Crypto, crypto))));
805
- const cluster = Context.get(context, ActorCluster);
806
- yield* cluster.ready;
807
- const tenant = yield* cluster.on(0)(ActorTest.use((test) => Effect.succeed(test.tenant)));
808
- const other = `${tenant}-b`;
809
- const total = (scoped, runner, by) => cluster.on(runner)(Effect.gen(function* () {
810
- const mirror = yield* Mirror.get("shared-id").pipe(Actor.tenant(scoped));
811
- yield* mirror.Bump(by);
812
- }));
813
- yield* total(tenant, 0, 1);
814
- yield* total(other, 1, 10);
815
- const watchOf = (scoped, runner) => cluster.on(runner)(Effect.gen(function* () {
816
- const mirror = yield* Mirror.get("shared-id").pipe(Actor.tenant(scoped));
817
- return yield* observe(mirror.Total.watch());
818
- }));
819
- const mine = yield* watchOf(tenant, 0);
820
- const theirs = yield* watchOf(other, 1);
821
- yield* mine.until(1);
822
- yield* theirs.until(1);
823
- expect([mine.seen, theirs.seen]).toEqual([[1], [10]]);
824
- yield* total(tenant, 1, 2);
825
- yield* mine.until(2);
826
- yield* Effect.sleep(QUIET);
827
- expect([mine.seen, theirs.seen]).toEqual([[1, 3], [10]]);
828
- }).pipe(Effect.scoped)),
829
- },
830
- {
831
- name: "a watch held on runner A sees a turn committed on runner B",
832
- requiresFreshDatabase: true,
833
- requiresIndependentConnections: true,
834
- timeoutMs: 90_000,
835
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 2, Effect.gen(function* () {
836
- const cluster = yield* ActorCluster;
837
- yield* cluster.ready;
838
- const ref = yield* ownedBy(1, "watch-cross");
839
- const held = yield* cluster.on(0)(Mirror.get(ref.id));
840
- yield* cluster.on(0)(held.Bump(1));
841
- const watch = yield* observe(held.Total.watch()).pipe(cluster.on(0));
842
- yield* watch.until(1);
843
- const writer = yield* cluster.on(1)(Mirror.get(ref.id));
844
- yield* cluster.on(1)(writer.Bump(2));
845
- yield* watch.until(2);
846
- expect(watch.seen).toEqual([1, 3]);
847
- expect(yield* cluster.owner(ref)).toBe(1);
848
- })),
849
- },
850
- {
851
- name: "reruns after an owner killed between commit and flush",
852
- requiresFreshDatabase: true,
853
- requiresIndependentConnections: true,
854
- timeoutMs: 120_000,
855
- run: ({ expect, environment, fixture }) => withCluster(environment, fixture, 3, Effect.gen(function* () {
856
- const cluster = yield* ActorCluster;
857
- yield* cluster.ready;
858
- const holder = cluster.on(0);
859
- const ref = yield* ownedBy(1, "watch-killed");
860
- const gauge = yield* holder(Mirror.get(ref.id));
861
- yield* holder(gauge.Bump(1));
862
- const watch = yield* observe(gauge.Total.watch()).pipe(holder);
863
- yield* watch.until(1);
864
- const owner = (yield* cluster.owner(ref));
865
- const paused = yield* cluster.on(owner)(ActorTest.use((test) => test.pauseNext("beforeFlush")));
866
- yield* holder(gauge.Bump(1)).pipe(Effect.ignore, Effect.forkChild({ startImmediately: true }));
867
- yield* paused.reached;
868
- yield* cluster.kill(owner);
869
- yield* watch.until(2);
870
- expect(watch.seen).toEqual([1, 2]);
871
- }), [0]),
872
- },
873
- ];
874
- /** Watched actors, served beside the HTTP actors. */
875
- export const watchSuite = {
876
- fixture: watchFixture,
877
- layer: watchLayer,
878
- uses: [httpSuite],
879
- };