@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.
- package/CHANGELOG.md +46 -3
- package/README.md +2 -2
- package/dist/actor/definition.d.ts +13 -9
- package/dist/actor/descriptor.d.ts +2 -2
- package/dist/actor/descriptor.js +4 -4
- package/dist/client/calls.js +1 -1
- package/dist/client/index.d.ts +1 -1
- package/dist/client/index.js +1 -1
- package/dist/errors/actor.d.ts +12 -42
- package/dist/errors/actor.js +13 -42
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/members/payload.d.ts +1 -1
- package/dist/protocol/inspection.d.ts +221 -1
- package/dist/protocol/inspection.js +151 -2
- package/dist/protocol/wire.d.ts +6 -49
- package/dist/protocol/wire.js +1 -10
- package/dist/runtime/actors.d.ts +3 -0
- package/dist/runtime/adoption/backfill.js +1 -1
- package/dist/runtime/adoption/enforce.js +2 -2
- package/dist/runtime/adoption/observe.js +1 -1
- package/dist/runtime/adoption/plan.d.ts +1 -1
- package/dist/runtime/adoption/startup.js +1 -1
- package/dist/runtime/adoption/status.d.ts +1 -1
- package/dist/runtime/app.d.ts +65 -0
- package/dist/runtime/app.js +38 -0
- package/dist/runtime/connections/holder.d.ts +11 -1
- package/dist/runtime/connections/holder.js +8 -4
- package/dist/runtime/connections/streams.d.ts +4 -4
- package/dist/runtime/content/store.js +15 -11
- package/dist/runtime/database/bounded.d.ts +4 -18
- package/dist/runtime/database/bounded.js +36 -22
- package/dist/runtime/database/coordination.d.ts +12 -3
- package/dist/runtime/database/coordination.js +29 -36
- package/dist/runtime/database/migrations.d.ts +61 -29
- package/dist/runtime/database/migrations.js +375 -139
- package/dist/runtime/database/replica.d.ts +4 -1
- package/dist/runtime/database/replica.js +4 -1
- package/dist/runtime/database/schema.d.ts +13 -0
- package/dist/runtime/database/schema.js +13 -0
- package/dist/runtime/database/shards.d.ts +31 -13
- package/dist/runtime/database/shards.js +38 -26
- package/dist/runtime/entity/register.d.ts +8 -1
- package/dist/runtime/entity/register.js +58 -7
- package/dist/runtime/fleet/checks.js +4 -4
- package/dist/runtime/fleet/maintainer.d.ts +1 -1
- package/dist/runtime/fleet/maintainer.js +1 -1
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +1 -0
- package/dist/runtime/inspector/layer.d.ts +20 -0
- package/dist/runtime/inspector/layer.js +224 -5
- package/dist/runtime/inspector/queries.d.ts +54 -8
- package/dist/runtime/inspector/queries.js +84 -15
- package/dist/runtime/layer.d.ts +48 -39
- package/dist/runtime/layer.js +124 -56
- package/dist/runtime/operators/audit.d.ts +3 -0
- package/dist/runtime/operators/audit.js +6 -2
- package/dist/runtime/operators/repair.js +30 -10
- package/dist/runtime/operators/seed.js +4 -2
- package/dist/runtime/payloads/versions.d.ts +4 -4
- package/dist/runtime/payloads/versions.js +24 -15
- package/dist/runtime/queries.js +4 -2
- package/dist/runtime/registration.d.ts +5 -2
- package/dist/runtime/registration.js +3 -3
- package/dist/runtime/runner.d.ts +12 -0
- package/dist/runtime/runner.js +12 -0
- package/dist/runtime/storage/codec.d.ts +19 -7
- package/dist/runtime/storage/codec.js +23 -7
- package/dist/runtime/storage/placements.d.ts +6 -1
- package/dist/runtime/storage/placements.js +98 -6
- package/dist/runtime/storage/retention.d.ts +6 -0
- package/dist/runtime/storage/retention.js +25 -13
- package/dist/runtime/subscriptions/relay.js +47 -44
- package/dist/runtime/subscriptions/storage.d.ts +3 -2
- package/dist/runtime/subscriptions/storage.js +9 -6
- package/dist/runtime/telemetry/live.d.ts +255 -0
- package/dist/runtime/telemetry/live.js +540 -0
- package/dist/runtime/telemetry/metrics.d.ts +2 -0
- package/dist/runtime/telemetry/metrics.js +4 -1
- package/dist/runtime/telemetry/sampler.js +33 -13
- package/dist/runtime/telemetry/spans.d.ts +1 -1
- package/dist/runtime/telemetry/spans.js +1 -1
- package/dist/runtime/topology/locks.d.ts +6 -3
- package/dist/runtime/topology/locks.js +27 -4
- package/dist/runtime/turn/execute.d.ts +2 -0
- package/dist/runtime/turn/execute.js +5 -2
- package/dist/runtime/turn/pipeline.d.ts +5 -12
- package/dist/runtime/turn/pipeline.js +11 -11
- package/dist/runtime/turn/relay.d.ts +13 -3
- package/dist/runtime/turn/relay.js +27 -14
- package/dist/runtime/turn/rows.js +4 -4
- package/dist/runtime/workflows/compatibility.d.ts +1 -1
- package/dist/runtime/workflows/compatibility.js +85 -31
- package/dist/runtime/workflows/engine.js +20 -17
- package/dist/serve/layer.js +3 -2
- package/dist/tables/owned.d.ts +1 -1
- package/dist/tables/owned.js +1 -1
- package/dist/testing/actor-test.d.ts +2 -24
- package/dist/testing/actor-test.js +1 -27
- package/dist/testing/index.d.ts +0 -12
- package/dist/testing/index.js +0 -6
- package/package.json +1 -1
- package/dist/runtime/database/neki/migrations.d.ts +0 -31
- package/dist/runtime/database/neki/migrations.js +0 -276
- package/dist/runtime/database/neki/session.d.ts +0 -23
- package/dist/runtime/database/neki/session.js +0 -31
- package/dist/testing/cluster.d.ts +0 -93
- package/dist/testing/cluster.js +0 -296
- package/dist/testing/conformance/access.d.ts +0 -5
- package/dist/testing/conformance/access.js +0 -531
- package/dist/testing/conformance/admission.d.ts +0 -11
- package/dist/testing/conformance/admission.js +0 -182
- package/dist/testing/conformance/adoption.d.ts +0 -2
- package/dist/testing/conformance/adoption.js +0 -1019
- package/dist/testing/conformance/assertions.d.ts +0 -202
- package/dist/testing/conformance/assertions.js +0 -849
- package/dist/testing/conformance/batches.d.ts +0 -41
- package/dist/testing/conformance/batches.js +0 -388
- package/dist/testing/conformance/blobs.d.ts +0 -14
- package/dist/testing/conformance/blobs.js +0 -405
- package/dist/testing/conformance/capacity.d.ts +0 -8
- package/dist/testing/conformance/capacity.js +0 -433
- package/dist/testing/conformance/client.d.ts +0 -5
- package/dist/testing/conformance/client.js +0 -719
- package/dist/testing/conformance/cold-serve.d.ts +0 -16
- package/dist/testing/conformance/cold-serve.js +0 -254
- package/dist/testing/conformance/connections/actors.d.ts +0 -1143
- package/dist/testing/conformance/connections/actors.js +0 -180
- package/dist/testing/conformance/connections/harness.d.ts +0 -1754
- package/dist/testing/conformance/connections/harness.js +0 -142
- package/dist/testing/conformance/connections/holders.d.ts +0 -4
- package/dist/testing/conformance/connections/holders.js +0 -165
- package/dist/testing/conformance/connections/lifecycle.d.ts +0 -4
- package/dist/testing/conformance/connections/lifecycle.js +0 -113
- package/dist/testing/conformance/connections/limits.d.ts +0 -4
- package/dist/testing/conformance/connections/limits.js +0 -127
- package/dist/testing/conformance/connections/reauthorization.d.ts +0 -4
- package/dist/testing/conformance/connections/reauthorization.js +0 -170
- package/dist/testing/conformance/connections/resync.d.ts +0 -4
- package/dist/testing/conformance/connections/resync.js +0 -360
- package/dist/testing/conformance/connections/takeover.d.ts +0 -4
- package/dist/testing/conformance/connections/takeover.js +0 -287
- package/dist/testing/conformance/connections.d.ts +0 -4
- package/dist/testing/conformance/connections.js +0 -15
- package/dist/testing/conformance/content-blobs.d.ts +0 -16
- package/dist/testing/conformance/content-blobs.js +0 -534
- package/dist/testing/conformance/counter.d.ts +0 -16
- package/dist/testing/conformance/counter.js +0 -813
- package/dist/testing/conformance/cron.d.ts +0 -5
- package/dist/testing/conformance/cron.js +0 -665
- package/dist/testing/conformance/drain.d.ts +0 -26
- package/dist/testing/conformance/drain.js +0 -540
- package/dist/testing/conformance/effect-control.d.ts +0 -38
- package/dist/testing/conformance/effect-control.js +0 -1059
- package/dist/testing/conformance/effects.d.ts +0 -23
- package/dist/testing/conformance/effects.js +0 -432
- package/dist/testing/conformance/events.d.ts +0 -28
- package/dist/testing/conformance/events.js +0 -335
- package/dist/testing/conformance/export.d.ts +0 -2
- package/dist/testing/conformance/export.js +0 -368
- package/dist/testing/conformance/fleet.d.ts +0 -444
- package/dist/testing/conformance/fleet.js +0 -1009
- package/dist/testing/conformance/groups.d.ts +0 -7
- package/dist/testing/conformance/groups.js +0 -353
- package/dist/testing/conformance/heap.d.ts +0 -3
- package/dist/testing/conformance/heap.js +0 -149
- package/dist/testing/conformance/http.d.ts +0 -2809
- package/dist/testing/conformance/http.js +0 -1234
- package/dist/testing/conformance/inspection-views.d.ts +0 -8
- package/dist/testing/conformance/inspection-views.js +0 -272
- package/dist/testing/conformance/inspector.d.ts +0 -7
- package/dist/testing/conformance/inspector.js +0 -650
- package/dist/testing/conformance/mint.d.ts +0 -58
- package/dist/testing/conformance/mint.js +0 -692
- package/dist/testing/conformance/multi-runner.d.ts +0 -8
- package/dist/testing/conformance/multi-runner.js +0 -403
- package/dist/testing/conformance/neki/plan.d.ts +0 -19
- package/dist/testing/conformance/neki/plan.js +0 -30
- package/dist/testing/conformance/observability.d.ts +0 -3
- package/dist/testing/conformance/observability.js +0 -343
- package/dist/testing/conformance/offline.d.ts +0 -4
- package/dist/testing/conformance/offline.js +0 -474
- package/dist/testing/conformance/operator-harness.d.ts +0 -57
- package/dist/testing/conformance/operator-harness.js +0 -78
- package/dist/testing/conformance/operator.d.ts +0 -3
- package/dist/testing/conformance/operator.js +0 -379
- package/dist/testing/conformance/outbox-cross-shard.d.ts +0 -14
- package/dist/testing/conformance/outbox-cross-shard.js +0 -160
- package/dist/testing/conformance/outbox.d.ts +0 -36
- package/dist/testing/conformance/outbox.js +0 -437
- package/dist/testing/conformance/payload-migrations.d.ts +0 -3
- package/dist/testing/conformance/payload-migrations.js +0 -612
- package/dist/testing/conformance/pipeline.d.ts +0 -3
- package/dist/testing/conformance/pipeline.js +0 -1040
- package/dist/testing/conformance/placement.d.ts +0 -22
- package/dist/testing/conformance/placement.js +0 -539
- package/dist/testing/conformance/platform.d.ts +0 -40
- package/dist/testing/conformance/platform.js +0 -101
- package/dist/testing/conformance/progress.d.ts +0 -39
- package/dist/testing/conformance/progress.js +0 -666
- package/dist/testing/conformance/properties.d.ts +0 -6
- package/dist/testing/conformance/properties.js +0 -295
- package/dist/testing/conformance/protocols.d.ts +0 -4
- package/dist/testing/conformance/protocols.js +0 -618
- package/dist/testing/conformance/provider/advertise.d.ts +0 -21
- package/dist/testing/conformance/provider/advertise.js +0 -44
- package/dist/testing/conformance/read-your-writes.d.ts +0 -20
- package/dist/testing/conformance/read-your-writes.js +0 -192
- package/dist/testing/conformance/reducers.d.ts +0 -8
- package/dist/testing/conformance/reducers.js +0 -258
- package/dist/testing/conformance/relay.d.ts +0 -42
- package/dist/testing/conformance/relay.js +0 -1115
- package/dist/testing/conformance/restore.d.ts +0 -43
- package/dist/testing/conformance/restore.js +0 -341
- package/dist/testing/conformance/retention.d.ts +0 -14
- package/dist/testing/conformance/retention.js +0 -463
- package/dist/testing/conformance/rls.d.ts +0 -24
- package/dist/testing/conformance/rls.js +0 -576
- package/dist/testing/conformance/simulation.d.ts +0 -31
- package/dist/testing/conformance/simulation.js +0 -167
- package/dist/testing/conformance/single-shard.d.ts +0 -2
- package/dist/testing/conformance/single-shard.js +0 -125
- package/dist/testing/conformance/singleton.d.ts +0 -3
- package/dist/testing/conformance/singleton.js +0 -293
- package/dist/testing/conformance/statements.d.ts +0 -60
- package/dist/testing/conformance/statements.js +0 -108
- package/dist/testing/conformance/streams.d.ts +0 -7
- package/dist/testing/conformance/streams.js +0 -280
- package/dist/testing/conformance/subscriptions/access.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/access.js +0 -98
- package/dist/testing/conformance/subscriptions/actors.d.ts +0 -4910
- package/dist/testing/conformance/subscriptions/actors.js +0 -372
- package/dist/testing/conformance/subscriptions/cluster.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/cluster.js +0 -89
- package/dist/testing/conformance/subscriptions/delivery.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/delivery.js +0 -253
- package/dist/testing/conformance/subscriptions/deploys.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/deploys.js +0 -162
- package/dist/testing/conformance/subscriptions/epochs.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/epochs.js +0 -169
- package/dist/testing/conformance/subscriptions/failures.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/failures.js +0 -108
- package/dist/testing/conformance/subscriptions/feeds.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/feeds.js +0 -218
- package/dist/testing/conformance/subscriptions/harness.d.ts +0 -124
- package/dist/testing/conformance/subscriptions/harness.js +0 -143
- package/dist/testing/conformance/subscriptions/operator.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/operator.js +0 -128
- package/dist/testing/conformance/subscriptions/retention.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/retention.js +0 -288
- package/dist/testing/conformance/subscriptions/routing.d.ts +0 -4
- package/dist/testing/conformance/subscriptions/routing.js +0 -179
- package/dist/testing/conformance/subscriptions.d.ts +0 -4
- package/dist/testing/conformance/subscriptions.js +0 -19
- package/dist/testing/conformance/tables.d.ts +0 -125
- package/dist/testing/conformance/tables.js +0 -664
- package/dist/testing/conformance/transports/actors.d.ts +0 -1907
- package/dist/testing/conformance/transports/actors.js +0 -163
- package/dist/testing/conformance/transports/client.d.ts +0 -3
- package/dist/testing/conformance/transports/client.js +0 -237
- package/dist/testing/conformance/transports/feeds.d.ts +0 -3
- package/dist/testing/conformance/transports/feeds.js +0 -232
- package/dist/testing/conformance/transports/harness.d.ts +0 -93
- package/dist/testing/conformance/transports/harness.js +0 -84
- package/dist/testing/conformance/transports/streams.d.ts +0 -3
- package/dist/testing/conformance/transports/streams.js +0 -96
- package/dist/testing/conformance/transports/websocket.d.ts +0 -3
- package/dist/testing/conformance/transports/websocket.js +0 -384
- package/dist/testing/conformance/transports/wire.d.ts +0 -141
- package/dist/testing/conformance/transports/wire.js +0 -204
- package/dist/testing/conformance/transports.d.ts +0 -3
- package/dist/testing/conformance/transports.js +0 -11
- package/dist/testing/conformance/watch.d.ts +0 -59
- package/dist/testing/conformance/watch.js +0 -879
- package/dist/testing/conformance/workflow-engine.d.ts +0 -97
- package/dist/testing/conformance/workflow-engine.js +0 -253
- package/dist/testing/conformance/workflow-versions.d.ts +0 -3
- package/dist/testing/conformance/workflow-versions.js +0 -545
- package/dist/testing/conformance/workflows/access.d.ts +0 -4
- package/dist/testing/conformance/workflows/access.js +0 -147
- package/dist/testing/conformance/workflows/actors.d.ts +0 -2317
- package/dist/testing/conformance/workflows/actors.js +0 -305
- package/dist/testing/conformance/workflows/basics.d.ts +0 -4
- package/dist/testing/conformance/workflows/basics.js +0 -144
- package/dist/testing/conformance/workflows/engine.d.ts +0 -4
- package/dist/testing/conformance/workflows/engine.js +0 -80
- package/dist/testing/conformance/workflows/guards.d.ts +0 -4
- package/dist/testing/conformance/workflows/guards.js +0 -172
- package/dist/testing/conformance/workflows/harness.d.ts +0 -14
- package/dist/testing/conformance/workflows/harness.js +0 -44
- package/dist/testing/conformance/workflows/recovery.d.ts +0 -4
- package/dist/testing/conformance/workflows/recovery.js +0 -181
- package/dist/testing/conformance/workflows/starts.d.ts +0 -4
- package/dist/testing/conformance/workflows/starts.js +0 -57
- package/dist/testing/conformance/workflows/suspension.d.ts +0 -4
- package/dist/testing/conformance/workflows/suspension.js +0 -106
- package/dist/testing/conformance/workflows/waits.d.ts +0 -4
- package/dist/testing/conformance/workflows/waits.js +0 -284
- package/dist/testing/conformance/workflows.d.ts +0 -4
- package/dist/testing/conformance/workflows.js +0 -19
- package/dist/testing/conformance.d.ts +0 -330
- package/dist/testing/conformance.js +0 -364
- package/dist/testing/foundation.d.ts +0 -41
- package/dist/testing/foundation.js +0 -517
- package/dist/testing/simulate-cluster.d.ts +0 -112
- package/dist/testing/simulate-cluster.js +0 -257
- package/dist/testing/simulate.d.ts +0 -90
- 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
|
-
};
|