@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,650 +0,0 @@
1
- import { Context, Crypto, Data, Effect, Exit, Layer, Option, Schedule, Schema } from "effect";
2
- import { FetchHttpClient, Headers, HttpClient, HttpClientRequest, HttpRouter } from "effect/http";
3
- import { SqlClient } from "effect/sql";
4
- import { Actor, Intent, Unauthorized, User } from "../../index.js";
5
- import { Inspector } from "../../runtime/index.js";
6
- import * as Queries from "../../runtime/inspector/queries.js";
7
- import { ActorTest } from "../actor-test.js";
8
- import { Auth } from "../../runtime/index.js";
9
- import { serveFetch, zstdDecompress } from "./platform.js";
10
- const Noted = Actor.event("Noted", { body: Schema.String });
11
- class Rejected extends Schema.TaggedError()("Rejected", {}) {
12
- }
13
- class Undeliverable extends Schema.TaggedError()("Undeliverable", {}) {
14
- }
15
- const Notify = Actor.job("Notify", { payload: { body: Schema.String } });
16
- const Write = Actor.command("Write", { payload: Schema.String });
17
- const WriteThenReject = Actor.command("WriteThenReject", {
18
- payload: Schema.String,
19
- error: Rejected,
20
- });
21
- const WriteThenDie = Actor.command("WriteThenDie", { payload: Schema.String });
22
- const Nudge = Actor.command("Nudge");
23
- const Settle = Actor.workflow("Settle", {
24
- payload: { order: Schema.String },
25
- success: Schema.String,
26
- key: ({ order }) => order,
27
- });
28
- const Reserve = Settle.step("reserve", { payload: Schema.String, success: Schema.String });
29
- const Pause = Settle.sleep("pause");
30
- const Inspected = Actor.make("Inspected", {
31
- key: Schema.String,
32
- state: Actor.state({
33
- notes: Schema.Array(Schema.String).pipe(Schema.withDecodingDefault(Effect.succeed([]))),
34
- }),
35
- events: [Noted],
36
- jobs: { Notify: { job: Notify, retry: { times: 0 } } },
37
- api: { Write, WriteThenReject, WriteThenDie, Settle },
38
- internal: { Nudge },
39
- });
40
- const write = Effect.fnUntraced(function* (body) {
41
- const turn = yield* Inspected.Turn;
42
- yield* turn.state.set({ notes: [...turn.state.notes, body] });
43
- yield* turn.emit(Noted.make({ body }));
44
- const self = yield* Inspected.intents(turn.id);
45
- yield* self.Nudge().pipe(Intent.after("1 hour"), Intent.key("nudge"));
46
- yield* turn.enqueue(Notify.make({ body }));
47
- });
48
- export const inspectorLayer = Layer.mergeAll(Inspected.toLayer(Effect.succeed({
49
- Write: write,
50
- WriteThenReject: Effect.fnUntraced(function* (body) {
51
- yield* write(body);
52
- return yield* Rejected.make({});
53
- }),
54
- WriteThenDie: Effect.fnUntraced(function* (body) {
55
- yield* write(body);
56
- return yield* Effect.die(new Error("Inspected defect after writing"));
57
- }),
58
- Nudge: () => Effect.void,
59
- Settle: Effect.fnUntraced(function* ({ order }) {
60
- const reserved = yield* Reserve.run(order, (id) => Effect.succeed(`held-${id}`));
61
- yield* Pause("10 seconds");
62
- return `${reserved}:settled`;
63
- }),
64
- })), Inspected.toJobLayer(Effect.succeed({
65
- Notify: Effect.fnUntraced(function* () {
66
- return yield* Undeliverable.make({});
67
- }),
68
- })));
69
- /** How many requests reached the operators' provider. */
70
- const reached = { count: 0 };
71
- const operators = Auth.make((request) => Option.match(Headers.get(request.headers, "authorization").pipe(Option.map((header) => {
72
- reached.count += 1;
73
- return header;
74
- })), {
75
- onNone: () => Effect.fail(Unauthorized.make({ code: "missing_credentials" })),
76
- onSome: (header) => {
77
- const match = /^Bearer ([A-Za-z0-9._:-]+)$/.exec(header);
78
- return match === null
79
- ? Effect.fail(Unauthorized.make({ code: "invalid_credentials" }))
80
- : Effect.succeed({ tenant: match[1], caller: User.make({ subject: "operator" }) });
81
- },
82
- }));
83
- const decodeJson = Schema.decodeUnknownEffect(Schema.fromJsonString(Schema.Json));
84
- /** Serves the inspector from a real listening server for the rest of the scope. */
85
- const serveInspector = Effect.gen(function* () {
86
- const context = yield* Effect.context();
87
- const web = HttpRouter.toWebHandler(Inspector.serve({ auth: operators }).pipe(Layer.provide(Layer.succeedContext(context))), { disableLogger: true });
88
- yield* Effect.addFinalizer(() => Effect.promise(() => web.dispose()));
89
- const port = yield* serveFetch((request) => web.handler(request));
90
- const client = Context.get(yield* Layer.build(FetchHttpClient.layer), HttpClient.HttpClient);
91
- const url = `http://127.0.0.1:${port}`;
92
- return (path, tenant, origin) => Effect.gen(function* () {
93
- const request = HttpClientRequest.get(`${url}/inspector${path}`, {
94
- headers: origin === undefined ? {} : { origin: origin === "self" ? url : origin },
95
- });
96
- const response = yield* client.execute(tenant === undefined ? request : HttpClientRequest.bearerToken(request, tenant));
97
- return { status: response.status, body: yield* decodeJson(yield* response.text) };
98
- }).pipe(Effect.orDie);
99
- });
100
- const isRecord = Schema.is(Schema.Record(Schema.String, Schema.Json));
101
- const isArray = Schema.is(Schema.Array(Schema.Json));
102
- const isNumber = Schema.is(Schema.Finite);
103
- /** A field of a decoded JSON body, without trusting its shape. */
104
- const field = (value, ...path) => {
105
- let current = value;
106
- for (const key of path)
107
- current = isArray(current)
108
- ? (current[Number(key)] ?? null)
109
- : isRecord(current)
110
- ? (current[key] ?? null)
111
- : null;
112
- return current;
113
- };
114
- const list = (value, ...path) => {
115
- const found = field(value, ...path);
116
- return isArray(found) ? found : [];
117
- };
118
- const text = (value) => (Schema.is(Schema.String)(value) ? value : "");
119
- const byText = (left, right) => left.localeCompare(right);
120
- const numeral = (value) => (isNumber(value) ? String(value) : "");
121
- const parseJson = Schema.decodeUnknownOption(Schema.fromJsonString(Schema.Json));
122
- const decodeJsonSync = (json) => Option.getOrThrow(parseJson(json));
123
- /** Independent decode of a compressed view value: the bytes as Postgres stores them. */
124
- const rawJson = (bytes) => decodeJsonSync(new TextDecoder().decode(zstdDecompress(bytes)));
125
- const eventually = (check, what) => check.pipe(Effect.repeat({ schedule: Schedule.spaced("25 millis"), until: (held) => held }), Effect.timeoutOrElse({
126
- duration: "30 seconds",
127
- orElse: () => Effect.die(new Error(`Timed out waiting for ${what}`)),
128
- }), Effect.asVoid);
129
- const tenantCounts = Effect.fnUntraced(function* (tenant) {
130
- const sql = yield* SqlClient.SqlClient;
131
- const [row] = yield* sql `
132
- SELECT
133
- (SELECT count(*)::int FROM durable.actors WHERE tenant_id = ${tenant}) AS actors,
134
- (SELECT count(*)::int FROM durable.receipts WHERE tenant_id = ${tenant}) AS receipts,
135
- (SELECT count(*)::int FROM durable.events WHERE tenant_id = ${tenant}) AS events,
136
- (SELECT count(*)::int FROM durable.outbox WHERE tenant_id = ${tenant}) AS outbox,
137
- (SELECT count(*)::int FROM durable.timers WHERE tenant_id = ${tenant}) AS timers,
138
- (SELECT count(*)::int FROM durable.jobs WHERE tenant_id = ${tenant}) AS jobs,
139
- (SELECT count(*)::int FROM durable.dead_letters WHERE tenant_id = ${tenant}) AS "deadLetters",
140
- (SELECT count(*)::int FROM durable.workflows WHERE tenant_id = ${tenant}) AS workflows,
141
- (SELECT count(*)::int FROM durable.workflows
142
- WHERE tenant_id = ${tenant} AND status <> 'finished') AS "openWorkflows"`;
143
- return row;
144
- });
145
- const RUNTIME_TABLES = [
146
- "actor_generations",
147
- "actor_state",
148
- "actor_receipts",
149
- "actor_events",
150
- "actor_outbox",
151
- "actor_dead_letters",
152
- "actor_workflow_executions",
153
- "actor_workflow_step",
154
- ];
155
- const fingerprint = Effect.gen(function* () {
156
- const sql = yield* SqlClient.SqlClient;
157
- const [row] = yield* sql.unsafe(`SELECT ${RUNTIME_TABLES.map((table) => `(SELECT count(*)::text || ':' || coalesce(md5(string_agg(t::text, ',' ORDER BY t::text)), '')
158
- FROM ${table} t WHERE t.actor_type = 'Inspected') AS ${table}`).join(", ")}`);
159
- return row;
160
- });
161
- const rejection = (exit) => Exit.isFailure(exit) ? String(exit.cause) : "succeeded";
162
- class Probed extends Data.TaggedError("Probed") {
163
- }
164
- /** Inspector cases: decoded committed rows and events, workflow step history, and tenant scoping by the authenticated principal. */
165
- export const inspectorConformance = [
166
- {
167
- name: "inspector: shows an actor's committed rows decoded, with the events each receipt committed",
168
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
169
- const test = yield* ActorTest;
170
- const sql = yield* SqlClient.SqlClient;
171
- const get = yield* serveInspector;
172
- const inspected = yield* Inspected.get("flow");
173
- yield* inspected.Write("first");
174
- yield* inspected.WriteThenReject("second").pipe(Effect.exit);
175
- yield* inspected.WriteThenDie("third").pipe(Effect.exit);
176
- yield* test.advance(0);
177
- const detail = yield* get("/actor?type=Inspected&id=flow", test.tenant);
178
- expect(detail.status).toBe(200);
179
- const [stored] = yield* sql `SELECT generation::int AS generation, created, last_event_sequence::int AS last_event_sequence
180
- FROM durable.actors
181
- WHERE tenant_id = ${test.tenant} AND actor_type = 'Inspected' AND actor_id = 'flow'`;
182
- expect(field(detail.body, "actor")).toEqual({
183
- actorType: "Inspected",
184
- actorId: "flow",
185
- placement: "tenant",
186
- generation: stored.generation,
187
- created: stored.created,
188
- lastEventSequence: stored.last_event_sequence,
189
- });
190
- expect(field(detail.body, "state")).toMatchObject([
191
- { key: "notes", value: { json: ["first"] } },
192
- ]);
193
- const [event] = yield* sql `
194
- SELECT value, command_id FROM durable.events
195
- WHERE tenant_id = ${test.tenant} AND actor_type = 'Inspected' AND actor_id = 'flow'`;
196
- expect(field(detail.body, "events")).toMatchObject([
197
- {
198
- sequence: 1,
199
- event: "Noted",
200
- commandId: event.command_id,
201
- value: { json: rawJson(event.value) },
202
- },
203
- ]);
204
- const receipts = list(detail.body, "receipts").toSorted((left, right) => text(field(left, "command")).localeCompare(text(field(right, "command"))));
205
- expect(receipts).toMatchObject([
206
- { command: "Write", commandId: event.command_id, outcomeTag: "Success", events: [1] },
207
- { command: "WriteThenReject", outcomeTag: "Failure", events: [] },
208
- ]);
209
- expect(field(detail.body, "outbox")).toMatchObject([
210
- {
211
- timerKey: "nudge",
212
- targetType: "Inspected",
213
- targetId: "flow",
214
- command: "Nudge",
215
- attempts: 0,
216
- },
217
- ]);
218
- const [dead] = yield* sql `
219
- SELECT payload FROM durable.dead_letters
220
- WHERE tenant_id = ${test.tenant} AND actor_type = 'Inspected' AND actor_id = 'flow'`;
221
- expect(field(detail.body, "jobs")).toEqual([]);
222
- expect(field(detail.body, "deadLetters")).toMatchObject([
223
- {
224
- job: "Notify",
225
- attempts: 1,
226
- ambiguous: false,
227
- payload: { json: decodeJsonSync(dead.payload) },
228
- },
229
- ]);
230
- expect(field(detail.body, "totals")).toEqual({
231
- receipts: 2,
232
- events: 1,
233
- outbox: 1,
234
- jobs: 0,
235
- deadLetters: 1,
236
- workflows: 0,
237
- });
238
- expect(list((yield* get("/actors?type=Inspected", test.tenant)).body, "actors").map((row) => field(row, "actorId"))).toContain("flow");
239
- const overview = yield* get("/overview", test.tenant);
240
- expect(field(overview.body, "tenant")).toBe(test.tenant);
241
- expect(field(overview.body, "counts")).toEqual(yield* tenantCounts(test.tenant));
242
- expect(field(overview.body, "views")).toEqual((yield* sql `
243
- SELECT view_name, version FROM durable.views ORDER BY view_name COLLATE "C"`).map(({ view_name, version }) => ({ view: view_name, version })));
244
- const missing = yield* get("/actor?type=Inspected&id=never-written", test.tenant);
245
- expect(missing.status).toBe(404);
246
- expect(field(missing.body, "_tag")).toBe("NotFound");
247
- expect((yield* get("/actors?limit=0", test.tenant)).status).toBe(400);
248
- })),
249
- },
250
- {
251
- name: "inspector: pages an actor's receipts, timeline and latest events, counts actor and job types, and finds actors by address prefix",
252
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
253
- const test = yield* ActorTest;
254
- const sql = yield* SqlClient.SqlClient;
255
- const get = yield* serveInspector;
256
- const tenant = `${test.tenant}-paged`;
257
- const paged = yield* Inspected.get("paged").pipe(Actor.tenant(tenant));
258
- yield* paged.Write("one");
259
- yield* paged.Write("two");
260
- yield* paged.WriteThenReject("three").pipe(Effect.exit);
261
- yield* (yield* Inspected.get("pages").pipe(Actor.tenant(tenant))).Write("other");
262
- yield* (yield* Inspected.get("unpaged").pipe(Actor.tenant(tenant))).Write("apart");
263
- yield* test.advance(0);
264
- const byCodeUnit = (left, right) => left < right ? -1 : left > right ? 1 : 0;
265
- const stored = yield* sql `SELECT actor_id, command_id, command, caller_key, outcome_tag, expires_at_ms::text
266
- FROM durable.receipts WHERE tenant_id = ${tenant}`;
267
- const receiptOrder = (left, right) => Number(right.expires_at_ms) - Number(left.expires_at_ms) ||
268
- byCodeUnit(left.actor_id, right.actor_id) ||
269
- byCodeUnit(left.command_id, right.command_id);
270
- const expectedReceipts = (rows) => rows.toSorted(receiptOrder).map((row) => ({
271
- actorId: row.actor_id,
272
- commandId: row.command_id,
273
- command: row.command,
274
- outcomeTag: row.outcome_tag,
275
- callerKey: { json: decodeJsonSync(row.caller_key) },
276
- expiresAtMs: Number(row.expires_at_ms),
277
- }));
278
- const pages = Effect.fnUntraced(function* (path, key, cursor) {
279
- const rows = [];
280
- let after = "";
281
- for (let page = 0; page < 50; page++) {
282
- const reply = yield* get(`${path}${after}`, tenant);
283
- expect([path, reply.status]).toEqual([path, 200]);
284
- rows.push(...list(reply.body, key));
285
- const next = field(reply.body, "next");
286
- if (next === null)
287
- return rows;
288
- after = cursor(next);
289
- }
290
- return yield* Effect.die(new Error(`${path} never reached its last page`));
291
- });
292
- const receiptCursor = (next) => `&afterExpiresAtMs=${numeral(field(next, "expiresAtMs"))}&afterType=${encodeURIComponent(text(field(next, "actorType")))}&afterId=${encodeURIComponent(text(field(next, "actorId")))}&afterCommandId=${encodeURIComponent(text(field(next, "commandId")))}`;
293
- const ownReceipts = yield* pages("/receipts?type=Inspected&id=paged&limit=1", "receipts", receiptCursor);
294
- expect(ownReceipts.length).toBe(3);
295
- expect(ownReceipts).toMatchObject(expectedReceipts(stored.filter((row) => row.actor_id === "paged")));
296
- expect(yield* pages("/receipts?limit=2", "receipts", receiptCursor)).toMatchObject(expectedReceipts(stored));
297
- expect(list((yield* get("/receipts?outcome=Failure", tenant)).body, "receipts")).toMatchObject(expectedReceipts(stored.filter((row) => row.outcome_tag === "Failure")));
298
- expect((yield* get("/receipts?type=Inspected&id=never", tenant)).status).toBe(404);
299
- expect((yield* get("/receipts?id=paged", tenant)).status).toBe(400);
300
- const cursorTail = "&afterType=Inspected&afterId=paged&afterCommandId=c";
301
- for (const path of [
302
- `/receipts?afterExpiresAtMs=1.5${cursorTail}`,
303
- `/receipts?afterExpiresAtMs=1e21${cursorTail}`,
304
- "/receipts?afterExpiresAtMs=1&afterType=Inspected",
305
- "/actors?afterType=Inspected",
306
- "/dead-letters?afterDeadAtMs=1",
307
- "/workflows?afterExecutionId=x",
308
- "/timeline?type=Inspected&id=paged&beforeSequence=2",
309
- ])
310
- expect([path, (yield* get(path, tenant)).status]).toEqual([path, 400]);
311
- const events = yield* sql `SELECT sequence::int AS sequence, event, command_id, emitted_at_ms::text
312
- FROM durable.events
313
- WHERE tenant_id = ${tenant} AND actor_type = 'Inspected' AND actor_id = 'paged'`;
314
- expect(events.length).toBe(2);
315
- const callerOf = (commandId) => {
316
- const receipt = stored.find((row) => row.actor_id === "paged" && row.command_id === commandId);
317
- return receipt === undefined ? null : { json: decodeJsonSync(receipt.caller_key) };
318
- };
319
- const commands = [...new Set(events.map((event) => event.command_id))].map((commandId) => {
320
- const own = events.filter((event) => event.command_id === commandId);
321
- return {
322
- kind: "command",
323
- sequence: Math.min(...own.map((event) => event.sequence)),
324
- name: stored.find((row) => row.command_id === commandId).command,
325
- commandId,
326
- callerKey: callerOf(commandId),
327
- atMs: Math.min(...own.map((event) => Number(event.emitted_at_ms))),
328
- };
329
- });
330
- const expectedTimeline = [
331
- ...events.map((event) => ({
332
- kind: "event",
333
- sequence: event.sequence,
334
- name: event.event,
335
- commandId: event.command_id,
336
- callerKey: callerOf(event.command_id),
337
- atMs: Number(event.emitted_at_ms),
338
- })),
339
- ...commands,
340
- ].toSorted((left, right) => right.sequence - left.sequence ||
341
- Number(right.kind === "event") - Number(left.kind === "event"));
342
- expect(expectedTimeline.map((entry) => entry.kind)).toEqual([
343
- "event",
344
- "command",
345
- "event",
346
- "command",
347
- ]);
348
- expect(yield* pages("/timeline?type=Inspected&id=paged&limit=3", "entries", (next) => `&beforeSequence=${numeral(field(next, "sequence"))}&beforeKind=${text(field(next, "kind"))}`)).toEqual(expectedTimeline);
349
- const newest = events.toSorted((left, right) => right.sequence - left.sequence)[0];
350
- expect(yield* pages("/latest-events?type=Inspected&id=paged&limit=1", "events", (next) => `&after=${encodeURIComponent(text(next))}`)).toEqual([
351
- {
352
- event: "Noted",
353
- sequence: newest.sequence,
354
- emittedAtMs: Number(newest.emitted_at_ms),
355
- },
356
- ]);
357
- const prefixed = (prefix) => get(`/actors?prefix=${encodeURIComponent(prefix)}`, tenant).pipe(Effect.map((reply) => list(reply.body, "actors").map((row) => field(row, "actorId"))));
358
- expect(yield* prefixed("Inspected/page")).toEqual(["paged", "pages"]);
359
- expect(yield* prefixed("Inspected/paged")).toEqual(["paged"]);
360
- expect(yield* prefixed("Insp")).toEqual(["paged", "pages", "unpaged"]);
361
- expect(yield* prefixed("Inspected/zz")).toEqual([]);
362
- expect(yield* prefixed("Other")).toEqual([]);
363
- expect(yield* pages("/actor-types?limit=1", "actorTypes", (next) => `&after=${text(next)}`)).toEqual([{ actorType: "Inspected", actors: 3 }]);
364
- expect(field((yield* get("/actor-types?prefix=Insp", tenant)).body, "actorTypes")).toEqual([{ actorType: "Inspected", actors: 3 }]);
365
- expect(field((yield* get("/actor-types?prefix=Zed", tenant)).body, "actorTypes")).toEqual([]);
366
- expect(field((yield* get("/actor-types?type=Missing", tenant)).body)).toMatchObject({
367
- actorTypes: [],
368
- next: null,
369
- });
370
- const pending = yield* sql `
371
- SELECT job, attempts::int AS attempts FROM durable.jobs WHERE tenant_id = ${tenant}`;
372
- const dead = yield* sql `
373
- SELECT job, job_id, dead_at_ms::text FROM durable.dead_letters
374
- WHERE tenant_id = ${tenant}`;
375
- expect(dead.length).toBe(4);
376
- expect(yield* pages("/job-types?limit=1", "jobTypes", (next) => `&after=${text(next)}`)).toEqual([...new Set([...pending, ...dead].map((row) => row.job))]
377
- .toSorted(byCodeUnit)
378
- .map((job) => ({
379
- job,
380
- queued: pending.filter((row) => row.job === job && row.attempts === 0).length,
381
- retrying: pending.filter((row) => row.job === job && row.attempts > 0).length,
382
- deadLetters: dead.filter((row) => row.job === job).length,
383
- })));
384
- expect((yield* pages("/dead-letters?limit=1", "deadLetters", (next) => `&afterDeadAtMs=${numeral(field(next, "deadAtMs"))}&afterJobId=${encodeURIComponent(text(field(next, "jobId")))}`)).map((row) => field(row, "jobId"))).toEqual(dead
385
- .toSorted((left, right) => Number(right.dead_at_ms) - Number(left.dead_at_ms) ||
386
- byCodeUnit(left.job_id, right.job_id))
387
- .map((row) => row.job_id));
388
- const [timer] = yield* sql `
389
- SELECT min(due_at_ms)::text AS due FROM durable.timers WHERE tenant_id = ${tenant}`;
390
- expect(timer.due).not.toBe(null);
391
- expect(field((yield* get("/overview", tenant)).body, "nextTimerDueAtMs")).toBe(Number(timer.due));
392
- })),
393
- },
394
- {
395
- name: "inspector: shows a workflow's step history while open and its result once finished",
396
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
397
- const test = yield* ActorTest;
398
- const sql = yield* SqlClient.SqlClient;
399
- const get = yield* serveInspector;
400
- const inspected = yield* Inspected.get("settling");
401
- const run = yield* inspected.Settle({ order: "o-1" });
402
- yield* eventually(Effect.gen(function* () {
403
- const rows = yield* sql `
404
- SELECT status FROM durable.workflows WHERE execution_id = ${run.executionId}`;
405
- return rows[0]?.status === "suspended";
406
- }).pipe(Effect.orDie), "the execution to suspend");
407
- const [reserve] = yield* sql `
408
- SELECT exit FROM durable.workflow_steps
409
- WHERE execution_id = ${run.executionId} AND step = 'reserve'`;
410
- const [started] = yield* sql `
411
- SELECT payload FROM durable.workflows WHERE execution_id = ${run.executionId}`;
412
- const open = yield* get("/actor?type=Inspected&id=settling", test.tenant);
413
- expect(field(open.body, "workflows")).toMatchObject([
414
- {
415
- executionId: run.executionId,
416
- workflow: "Settle",
417
- workflowKey: "o-1",
418
- status: "suspended",
419
- interrupt: false,
420
- result: null,
421
- finishedAtMs: null,
422
- steps: [
423
- { step: "reserve", kind: "activity", exit: { json: rawJson(reserve.exit) } },
424
- { step: "pause", kind: "clock", exit: null },
425
- ],
426
- },
427
- ]);
428
- expect(field(open.body, "workflows", 0, "payload")).toEqual({
429
- json: rawJson(started.payload),
430
- });
431
- expect(isNumber(field(open.body, "workflows", 0, "steps", 1, "dueAtMs"))).toBe(true);
432
- const listed = (status) => get(`/workflows?status=${status}`, test.tenant).pipe(Effect.map((reply) => list(reply.body, "workflows").map((workflow) => field(workflow, "executionId"))));
433
- expect(yield* listed("open")).toContain(run.executionId);
434
- yield* test.advance("11 seconds");
435
- expect(yield* run.result).toBe("held-o-1:settled");
436
- const [finished] = yield* sql `
437
- SELECT result FROM durable.workflows WHERE execution_id = ${run.executionId}`;
438
- const done = yield* get("/actor?type=Inspected&id=settling", test.tenant);
439
- expect(field(done.body, "workflows")).toMatchObject([
440
- {
441
- executionId: run.executionId,
442
- status: "finished",
443
- result: { json: rawJson(finished.result) },
444
- steps: [],
445
- },
446
- ]);
447
- expect(yield* listed("open")).not.toContain(run.executionId);
448
- expect(yield* listed("all")).toContain(run.executionId);
449
- expect(yield* listed("finished")).toContain(run.executionId);
450
- expect(yield* listed("completed")).toContain(run.executionId);
451
- expect(yield* listed("failed")).not.toContain(run.executionId);
452
- expect(yield* listed("suspended")).not.toContain(run.executionId);
453
- const flows = `${test.tenant}-flows`;
454
- const listedIn = (status) => get(`/workflows?status=${status}`, flows).pipe(Effect.map((reply) => list(reply.body, "workflows").map((workflow) => field(workflow, "executionId"))));
455
- const starts = yield* Effect.forEach(["a", "b"], (order) => Inspected.get(`flow-${order}`).pipe(Effect.flatMap((actor) => actor.Settle({ order: `o-${order}` })), Actor.tenant(flows)));
456
- const executions = starts.map((start) => start.executionId);
457
- yield* eventually(Effect.map(listedIn("suspended"), (ids) => executions.every((id) => ids.includes(id))), "both executions to suspend");
458
- expect(yield* listedIn("running")).toEqual([]);
459
- const everyExecution = [];
460
- let after = "";
461
- for (let page = 0; page < 10; page++) {
462
- const reply = (yield* get(`/workflows?status=all&limit=1${after}`, flows)).body;
463
- everyExecution.push(...list(reply, "workflows").map((row) => field(row, "executionId")));
464
- const next = field(reply, "next");
465
- if (next === null)
466
- break;
467
- after = `&afterStartedAtMs=${numeral(field(next, "startedAtMs"))}&afterExecutionId=${text(field(next, "executionId"))}`;
468
- }
469
- expect(everyExecution).toEqual(yield* listedIn("all"));
470
- expect(everyExecution.map(text).toSorted(byText)).toEqual(executions.toSorted(byText));
471
- })),
472
- },
473
- {
474
- name: "inspector: reads only the authenticated principal's tenant, refuses missing credentials, and refuses an oversized one before its provider runs",
475
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
476
- const test = yield* ActorTest;
477
- const get = yield* serveInspector;
478
- const home = `${test.tenant}-home`;
479
- const abroad = `${test.tenant}-inspected`;
480
- yield* (yield* Inspected.get("shared").pipe(Actor.tenant(home))).Write("home");
481
- yield* (yield* Inspected.get("shared").pipe(Actor.tenant(abroad))).Write("abroad");
482
- yield* (yield* Inspected.get("only-abroad").pipe(Actor.tenant(abroad))).Write("abroad");
483
- yield* test.advance(0);
484
- const shared = yield* get("/actor?type=Inspected&id=shared", home);
485
- expect(field(shared.body, "state")).toMatchObject([{ value: { json: ["home"] } }]);
486
- expect(field(shared.body, "totals", "receipts")).toBe(1);
487
- expect(yield* get(`/actor?type=Inspected&id=shared&tenant=${abroad}`, home)).toEqual(shared);
488
- expect((yield* get("/actor?type=Inspected&id=only-abroad", home)).status).toBe(404);
489
- expect(field((yield* get("/actor?type=Inspected&id=only-abroad", abroad)).body, "state")).toMatchObject([{ value: { json: ["abroad"] } }]);
490
- const homeActors = list((yield* get("/actors?type=Inspected", home)).body, "actors");
491
- expect(homeActors.map((row) => field(row, "actorId"))).not.toContain("only-abroad");
492
- expect(list((yield* get("/actors?type=Inspected", abroad)).body, "actors").map((row) => field(row, "actorId"))).toEqual(["only-abroad", "shared"]);
493
- for (const tenant of [home, abroad])
494
- expect(field((yield* get("/overview", tenant)).body, "counts")).toEqual(yield* tenantCounts(tenant));
495
- const abroadLetters = list((yield* get("/dead-letters", abroad)).body, "deadLetters");
496
- expect(abroadLetters
497
- .map((row) => text(field(row, "actorId")))
498
- .toSorted((left, right) => left.localeCompare(right))).toEqual(["only-abroad", "shared"]);
499
- for (const path of [
500
- "/receipts?type=Inspected&id=only-abroad",
501
- "/latest-events?type=Inspected&id=only-abroad",
502
- "/timeline?type=Inspected&id=only-abroad",
503
- ])
504
- expect((yield* get(path, home)).status).toBe(404);
505
- const sql = yield* SqlClient.SqlClient;
506
- const commandsOf = (tenant) => sql `
507
- SELECT command_id FROM durable.receipts
508
- WHERE tenant_id = ${tenant} AND actor_type = 'Inspected' AND actor_id = 'shared'`.pipe(Effect.map((rows) => rows.map((row) => row.command_id)));
509
- const homeCommands = yield* commandsOf(home);
510
- const abroadCommands = yield* commandsOf(abroad);
511
- expect(homeCommands.length).toBe(1);
512
- expect(abroadCommands.length).toBe(1);
513
- expect(homeCommands).not.toEqual(abroadCommands);
514
- const [homeEvent] = yield* sql `
515
- SELECT sequence::int AS sequence, command_id FROM durable.events
516
- WHERE tenant_id = ${home} AND actor_type = 'Inspected' AND actor_id = 'shared'`;
517
- expect(homeEvent.command_id).toBe(homeCommands[0]);
518
- const sharedAt = (path, key, name) => get(`/${path}?type=Inspected&id=shared`, home).pipe(Effect.map((reply) => list(reply.body, key).map((row) => field(row, name))));
519
- expect(yield* sharedAt("receipts", "receipts", "commandId")).toEqual(homeCommands);
520
- expect(yield* sharedAt("timeline", "entries", "commandId")).toEqual([
521
- homeCommands[0],
522
- homeCommands[0],
523
- ]);
524
- expect(yield* sharedAt("latest-events", "events", "sequence")).toEqual([
525
- homeEvent.sequence,
526
- ]);
527
- const receiptActors = (tenant) => get("/receipts", tenant).pipe(Effect.map((reply) => list(reply.body, "receipts")
528
- .map((row) => text(field(row, "actorId")))
529
- .toSorted((left, right) => left.localeCompare(right))));
530
- expect(yield* receiptActors(abroad)).toEqual(["only-abroad", "shared"]);
531
- expect(yield* receiptActors(home)).toEqual(["shared"]);
532
- expect(field((yield* get("/actor-types", abroad)).body, "actorTypes")).toEqual([
533
- { actorType: "Inspected", actors: 2 },
534
- ]);
535
- expect(field((yield* get("/actor-types", home)).body, "actorTypes")).toEqual([
536
- { actorType: "Inspected", actors: 1 },
537
- ]);
538
- expect(field((yield* get("/job-types", abroad)).body, "jobTypes")).toEqual([
539
- { job: "Notify", queued: 0, retrying: 0, deadLetters: 2 },
540
- ]);
541
- expect(field((yield* get("/job-types", home)).body, "jobTypes")).toEqual([
542
- { job: "Notify", queued: 0, retrying: 0, deadLetters: 1 },
543
- ]);
544
- expect(list((yield* get("/actors?prefix=Inspected/only", home)).body, "actors")).toEqual([]);
545
- for (const path of ["/overview", "/actors", "/actor?type=Inspected&id=shared"]) {
546
- const anonymous = yield* get(path);
547
- expect(anonymous.status).toBe(401);
548
- expect(field(anonymous.body, "reason", "_tag")).toBe("Unauthorized");
549
- expect(field(anonymous.body, "reason", "code")).toBe("missing_credentials");
550
- }
551
- const foreign = yield* get("/actor?type=Inspected&id=shared", home, "https://elsewhere.example");
552
- expect(foreign.status).toBe(403);
553
- expect(field(foreign.body, "reason", "code")).toBe("origin_not_allowed");
554
- expect(field(foreign.body, "state")).toBe(null);
555
- expect(yield* get("/actor?type=Inspected&id=shared", home, "self")).toEqual(shared);
556
- const before = reached.count;
557
- const oversized = yield* get("/overview", "a".repeat(9 * 1024));
558
- expect(oversized.status).toBe(413);
559
- expect(field(oversized.body, "reason", "code")).toBe("too_large");
560
- expect(reached.count).toBe(before);
561
- const everyone = list((yield* get("/actors?type=Inspected", abroad)).body, "actors");
562
- const paged = [];
563
- let cursor = "";
564
- for (let page = 0; page <= everyone.length; page++) {
565
- const reply = (yield* get(`/actors?type=Inspected&limit=1${cursor}`, abroad)).body;
566
- paged.push(...list(reply, "actors"));
567
- const next = field(reply, "next");
568
- if (next === null)
569
- break;
570
- cursor = `&afterType=${encodeURIComponent(text(field(next, "actorType")))}&afterId=${encodeURIComponent(text(field(next, "actorId")))}`;
571
- }
572
- expect(paged).toEqual(everyone);
573
- })),
574
- },
575
- {
576
- name: "inspector: reads through the durable views only and never writes",
577
- run: ({ expect, environment }) => environment.run(Effect.gen(function* () {
578
- const test = yield* ActorTest;
579
- const sql = yield* SqlClient.SqlClient;
580
- const get = yield* serveInspector;
581
- const inspected = yield* Inspected.get("untouched");
582
- yield* inspected.Write("kept");
583
- yield* test.advance(0);
584
- const before = yield* fingerprint;
585
- for (const path of [
586
- "/overview",
587
- "/actors",
588
- "/actor?type=Inspected&id=untouched",
589
- "/outbox",
590
- "/jobs",
591
- "/dead-letters",
592
- "/workflows?status=all",
593
- "/actor-types",
594
- "/job-types",
595
- "/receipts",
596
- "/receipts?type=Inspected&id=untouched",
597
- "/latest-events?type=Inspected&id=untouched",
598
- "/timeline?type=Inspected&id=untouched",
599
- ])
600
- expect((yield* get(path, test.tenant)).status).toBe(200);
601
- expect(yield* fingerprint).toEqual(before);
602
- const uuid = yield* (yield* Crypto.Crypto).randomUUIDv4.pipe(Effect.orDie);
603
- const role = `inspector_${uuid.replaceAll("-", "")}`;
604
- const probe = yield* sql
605
- .withTransaction(Effect.gen(function* () {
606
- yield* sql.unsafe(`CREATE ROLE ${role} NOLOGIN`);
607
- yield* sql.unsafe(`GRANT USAGE ON SCHEMA durable TO ${role}`);
608
- yield* sql.unsafe(`GRANT SELECT ON ALL TABLES IN SCHEMA durable TO ${role}`);
609
- yield* sql.unsafe(`SET LOCAL ROLE ${role}`);
610
- yield* sql `SET TRANSACTION READ ONLY`;
611
- const page = { tenant: test.tenant, limit: 50 };
612
- yield* Queries.overview(page);
613
- const listed = yield* Queries.actors(page);
614
- yield* Queries.outbox(page);
615
- yield* Queries.jobs(page);
616
- yield* Queries.deadLetters(page);
617
- yield* Queries.workflows({ ...page, status: "all" });
618
- yield* Queries.actorTypes(page);
619
- yield* Queries.jobTypes(page);
620
- yield* Queries.receipts(page);
621
- const untouched = { ...page, actorType: "Inspected", actorId: "untouched" };
622
- yield* Queries.receipts(untouched);
623
- yield* Queries.latestEvents(untouched);
624
- yield* Queries.timeline(untouched);
625
- const found = yield* Queries.actor({
626
- ...page,
627
- actorType: "Inspected",
628
- actorId: "untouched",
629
- });
630
- const denied = yield* sql `SELECT 1 FROM actor_receipts LIMIT 1`.pipe(sql.withTransaction, Effect.exit, Effect.map(rejection));
631
- return yield* new Probed({
632
- found: Option.isSome(found),
633
- listed: listed.actors.length,
634
- denied,
635
- });
636
- }))
637
- .pipe(Effect.catchTag("Probed", Effect.succeed));
638
- expect(probe.found).toBe(true);
639
- expect(probe.listed > 0).toBe(true);
640
- expect(/permission denied/.test(probe.denied)).toBe(true);
641
- const write = rejection(yield* Queries.readOnly(test.tenant)(sql `DELETE FROM actor_receipts`).pipe(Effect.exit));
642
- expect(/read-only transaction/.test(write)).toBe(true);
643
- expect(yield* fingerprint).toEqual(before);
644
- })),
645
- },
646
- ];
647
- /** Inspector actors. */
648
- export const inspectorSuite = {
649
- layer: () => inspectorLayer,
650
- };