@alchemy.run/effect-cluster 0.0.0 → 2.0.0-beta.86

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 (233) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +101 -4
  3. package/lib/AWS/Database.d.ts +54 -0
  4. package/lib/AWS/Database.d.ts.map +1 -0
  5. package/lib/AWS/Database.js +96 -0
  6. package/lib/AWS/Database.js.map +1 -0
  7. package/lib/AWS/EcsPool.d.ts +56 -0
  8. package/lib/AWS/EcsPool.d.ts.map +1 -0
  9. package/lib/AWS/EcsPool.js +87 -0
  10. package/lib/AWS/EcsPool.js.map +1 -0
  11. package/lib/AWS/index.d.ts +7 -0
  12. package/lib/AWS/index.d.ts.map +1 -0
  13. package/lib/AWS/index.js +7 -0
  14. package/lib/AWS/index.js.map +1 -0
  15. package/lib/AWS/internal/Gateway.d.ts +23 -0
  16. package/lib/AWS/internal/Gateway.d.ts.map +1 -0
  17. package/lib/AWS/internal/Gateway.js +70 -0
  18. package/lib/AWS/internal/Gateway.js.map +1 -0
  19. package/lib/Cloudflare/DurableObjectPool.d.ts +42 -0
  20. package/lib/Cloudflare/DurableObjectPool.d.ts.map +1 -0
  21. package/lib/Cloudflare/DurableObjectPool.js +136 -0
  22. package/lib/Cloudflare/DurableObjectPool.js.map +1 -0
  23. package/lib/Cloudflare/PoolWorker.d.ts +186 -0
  24. package/lib/Cloudflare/PoolWorker.d.ts.map +1 -0
  25. package/lib/Cloudflare/PoolWorker.js +75 -0
  26. package/lib/Cloudflare/PoolWorker.js.map +1 -0
  27. package/lib/Cloudflare/index.d.ts +7 -0
  28. package/lib/Cloudflare/index.d.ts.map +1 -0
  29. package/lib/Cloudflare/index.js +7 -0
  30. package/lib/Cloudflare/index.js.map +1 -0
  31. package/lib/Cloudflare/internal/Objects.d.ts +38 -0
  32. package/lib/Cloudflare/internal/Objects.d.ts.map +1 -0
  33. package/lib/Cloudflare/internal/Objects.js +102 -0
  34. package/lib/Cloudflare/internal/Objects.js.map +1 -0
  35. package/lib/Cron.d.ts +143 -0
  36. package/lib/Cron.d.ts.map +1 -0
  37. package/lib/Cron.js +83 -0
  38. package/lib/Cron.js.map +1 -0
  39. package/lib/Database.d.ts +95 -0
  40. package/lib/Database.d.ts.map +1 -0
  41. package/lib/Database.js +38 -0
  42. package/lib/Database.js.map +1 -0
  43. package/lib/Docker/Database.d.ts +42 -0
  44. package/lib/Docker/Database.d.ts.map +1 -0
  45. package/lib/Docker/Database.js +53 -0
  46. package/lib/Docker/Database.js.map +1 -0
  47. package/lib/Docker/SwarmPool.d.ts +44 -0
  48. package/lib/Docker/SwarmPool.d.ts.map +1 -0
  49. package/lib/Docker/SwarmPool.js +56 -0
  50. package/lib/Docker/SwarmPool.js.map +1 -0
  51. package/lib/Docker/index.d.ts +7 -0
  52. package/lib/Docker/index.d.ts.map +1 -0
  53. package/lib/Docker/index.js +7 -0
  54. package/lib/Docker/index.js.map +1 -0
  55. package/lib/Entity.d.ts +348 -0
  56. package/lib/Entity.d.ts.map +1 -0
  57. package/lib/Entity.js +308 -0
  58. package/lib/Entity.js.map +1 -0
  59. package/lib/Fly/Database.d.ts +50 -0
  60. package/lib/Fly/Database.d.ts.map +1 -0
  61. package/lib/Fly/Database.js +56 -0
  62. package/lib/Fly/Database.js.map +1 -0
  63. package/lib/Fly/MachinePool.d.ts +39 -0
  64. package/lib/Fly/MachinePool.d.ts.map +1 -0
  65. package/lib/Fly/MachinePool.js +51 -0
  66. package/lib/Fly/MachinePool.js.map +1 -0
  67. package/lib/Fly/index.d.ts +7 -0
  68. package/lib/Fly/index.d.ts.map +1 -0
  69. package/lib/Fly/index.js +7 -0
  70. package/lib/Fly/index.js.map +1 -0
  71. package/lib/GCP/ComputeEnginePool.d.ts +41 -0
  72. package/lib/GCP/ComputeEnginePool.d.ts.map +1 -0
  73. package/lib/GCP/ComputeEnginePool.js +172 -0
  74. package/lib/GCP/ComputeEnginePool.js.map +1 -0
  75. package/lib/GCP/Database.d.ts +48 -0
  76. package/lib/GCP/Database.d.ts.map +1 -0
  77. package/lib/GCP/Database.js +115 -0
  78. package/lib/GCP/Database.js.map +1 -0
  79. package/lib/GCP/index.d.ts +7 -0
  80. package/lib/GCP/index.d.ts.map +1 -0
  81. package/lib/GCP/index.js +7 -0
  82. package/lib/GCP/index.js.map +1 -0
  83. package/lib/Hetzner/Database.d.ts +48 -0
  84. package/lib/Hetzner/Database.d.ts.map +1 -0
  85. package/lib/Hetzner/Database.js +91 -0
  86. package/lib/Hetzner/Database.js.map +1 -0
  87. package/lib/Hetzner/ServerPool.d.ts +35 -0
  88. package/lib/Hetzner/ServerPool.d.ts.map +1 -0
  89. package/lib/Hetzner/ServerPool.js +98 -0
  90. package/lib/Hetzner/ServerPool.js.map +1 -0
  91. package/lib/Hetzner/index.d.ts +7 -0
  92. package/lib/Hetzner/index.d.ts.map +1 -0
  93. package/lib/Hetzner/index.js +7 -0
  94. package/lib/Hetzner/index.js.map +1 -0
  95. package/lib/Host.d.ts +60 -0
  96. package/lib/Host.d.ts.map +1 -0
  97. package/lib/Host.js +31 -0
  98. package/lib/Host.js.map +1 -0
  99. package/lib/Kubernetes/Database.d.ts +40 -0
  100. package/lib/Kubernetes/Database.d.ts.map +1 -0
  101. package/lib/Kubernetes/Database.js +56 -0
  102. package/lib/Kubernetes/Database.js.map +1 -0
  103. package/lib/Kubernetes/DeploymentPool.d.ts +54 -0
  104. package/lib/Kubernetes/DeploymentPool.d.ts.map +1 -0
  105. package/lib/Kubernetes/DeploymentPool.js +49 -0
  106. package/lib/Kubernetes/DeploymentPool.js.map +1 -0
  107. package/lib/Kubernetes/index.d.ts +7 -0
  108. package/lib/Kubernetes/index.d.ts.map +1 -0
  109. package/lib/Kubernetes/index.js +7 -0
  110. package/lib/Kubernetes/index.js.map +1 -0
  111. package/lib/Pool.d.ts +66 -0
  112. package/lib/Pool.d.ts.map +1 -0
  113. package/lib/Pool.js +47 -0
  114. package/lib/Pool.js.map +1 -0
  115. package/lib/Providers.d.ts +19 -0
  116. package/lib/Providers.d.ts.map +1 -0
  117. package/lib/Providers.js +31 -0
  118. package/lib/Providers.js.map +1 -0
  119. package/lib/ShardGroups.d.ts +24 -0
  120. package/lib/ShardGroups.d.ts.map +1 -0
  121. package/lib/ShardGroups.js +19 -0
  122. package/lib/ShardGroups.js.map +1 -0
  123. package/lib/Singleton.d.ts +110 -0
  124. package/lib/Singleton.d.ts.map +1 -0
  125. package/lib/Singleton.js +60 -0
  126. package/lib/Singleton.js.map +1 -0
  127. package/lib/Workflow.d.ts +251 -0
  128. package/lib/Workflow.d.ts.map +1 -0
  129. package/lib/Workflow.js +288 -0
  130. package/lib/Workflow.js.map +1 -0
  131. package/lib/index.d.ts +12 -0
  132. package/lib/index.d.ts.map +1 -0
  133. package/lib/index.js +12 -0
  134. package/lib/index.js.map +1 -0
  135. package/lib/internal/Bootstrap.d.ts +28 -0
  136. package/lib/internal/Bootstrap.d.ts.map +1 -0
  137. package/lib/internal/Bootstrap.js +257 -0
  138. package/lib/internal/Bootstrap.js.map +1 -0
  139. package/lib/internal/EntityRuntime.d.ts +40 -0
  140. package/lib/internal/EntityRuntime.d.ts.map +1 -0
  141. package/lib/internal/EntityRuntime.js +698 -0
  142. package/lib/internal/EntityRuntime.js.map +1 -0
  143. package/lib/internal/Gateway.d.ts +82 -0
  144. package/lib/internal/Gateway.d.ts.map +1 -0
  145. package/lib/internal/Gateway.js +218 -0
  146. package/lib/internal/Gateway.js.map +1 -0
  147. package/lib/internal/HostedPlatform.d.ts +36 -0
  148. package/lib/internal/HostedPlatform.d.ts.map +1 -0
  149. package/lib/internal/HostedPlatform.js +36 -0
  150. package/lib/internal/HostedPlatform.js.map +1 -0
  151. package/lib/internal/HostedRuntime.d.ts +58 -0
  152. package/lib/internal/HostedRuntime.d.ts.map +1 -0
  153. package/lib/internal/HostedRuntime.js +72 -0
  154. package/lib/internal/HostedRuntime.js.map +1 -0
  155. package/lib/internal/Naming.d.ts +12 -0
  156. package/lib/internal/Naming.d.ts.map +1 -0
  157. package/lib/internal/Naming.js +40 -0
  158. package/lib/internal/Naming.js.map +1 -0
  159. package/lib/internal/ObjectSharding.d.ts +141 -0
  160. package/lib/internal/ObjectSharding.d.ts.map +1 -0
  161. package/lib/internal/ObjectSharding.js +337 -0
  162. package/lib/internal/ObjectSharding.js.map +1 -0
  163. package/lib/internal/PoolHost.d.ts +80 -0
  164. package/lib/internal/PoolHost.d.ts.map +1 -0
  165. package/lib/internal/PoolHost.js +203 -0
  166. package/lib/internal/PoolHost.js.map +1 -0
  167. package/lib/internal/PoolHostService.d.ts +25 -0
  168. package/lib/internal/PoolHostService.d.ts.map +1 -0
  169. package/lib/internal/PoolHostService.js +10 -0
  170. package/lib/internal/PoolHostService.js.map +1 -0
  171. package/lib/internal/Wire.d.ts +60 -0
  172. package/lib/internal/Wire.d.ts.map +1 -0
  173. package/lib/internal/Wire.js +36 -0
  174. package/lib/internal/Wire.js.map +1 -0
  175. package/lib/internal/WorkflowGateway.d.ts +36 -0
  176. package/lib/internal/WorkflowGateway.d.ts.map +1 -0
  177. package/lib/internal/WorkflowGateway.js +124 -0
  178. package/lib/internal/WorkflowGateway.js.map +1 -0
  179. package/lib/internal/external-runner.d.ts +2 -0
  180. package/lib/internal/external-runner.d.ts.map +1 -0
  181. package/lib/internal/external-runner.js +7 -0
  182. package/lib/internal/external-runner.js.map +1 -0
  183. package/lib/internal/runner.d.ts +2 -0
  184. package/lib/internal/runner.d.ts.map +1 -0
  185. package/lib/internal/runner.js +5 -0
  186. package/lib/internal/runner.js.map +1 -0
  187. package/package.json +128 -3
  188. package/src/AWS/Database.ts +139 -0
  189. package/src/AWS/EcsPool.ts +118 -0
  190. package/src/AWS/index.ts +6 -0
  191. package/src/AWS/internal/Gateway.ts +82 -0
  192. package/src/Cloudflare/DurableObjectPool.ts +198 -0
  193. package/src/Cloudflare/PoolWorker.ts +124 -0
  194. package/src/Cloudflare/index.ts +6 -0
  195. package/src/Cloudflare/internal/Objects.ts +160 -0
  196. package/src/Cron.ts +202 -0
  197. package/src/Database.ts +116 -0
  198. package/src/Docker/Database.ts +94 -0
  199. package/src/Docker/SwarmPool.ts +83 -0
  200. package/src/Docker/index.ts +6 -0
  201. package/src/Entity.ts +621 -0
  202. package/src/Fly/Database.ts +102 -0
  203. package/src/Fly/MachinePool.ts +72 -0
  204. package/src/Fly/index.ts +6 -0
  205. package/src/GCP/ComputeEnginePool.ts +203 -0
  206. package/src/GCP/Database.ts +168 -0
  207. package/src/GCP/index.ts +6 -0
  208. package/src/Hetzner/Database.ts +142 -0
  209. package/src/Hetzner/ServerPool.ts +116 -0
  210. package/src/Hetzner/index.ts +6 -0
  211. package/src/Host.ts +68 -0
  212. package/src/Kubernetes/Database.ts +89 -0
  213. package/src/Kubernetes/DeploymentPool.ts +77 -0
  214. package/src/Kubernetes/index.ts +6 -0
  215. package/src/Pool.ts +104 -0
  216. package/src/Providers.ts +49 -0
  217. package/src/ShardGroups.ts +41 -0
  218. package/src/Singleton.ts +153 -0
  219. package/src/Workflow.ts +556 -0
  220. package/src/index.ts +11 -0
  221. package/src/internal/Bootstrap.ts +368 -0
  222. package/src/internal/EntityRuntime.ts +996 -0
  223. package/src/internal/Gateway.ts +366 -0
  224. package/src/internal/HostedPlatform.ts +83 -0
  225. package/src/internal/HostedRuntime.ts +118 -0
  226. package/src/internal/Naming.ts +44 -0
  227. package/src/internal/ObjectSharding.ts +578 -0
  228. package/src/internal/PoolHost.ts +330 -0
  229. package/src/internal/PoolHostService.ts +25 -0
  230. package/src/internal/Wire.ts +83 -0
  231. package/src/internal/WorkflowGateway.ts +188 -0
  232. package/src/internal/external-runner.ts +10 -0
  233. package/src/internal/runner.ts +5 -0
@@ -0,0 +1,698 @@
1
+ /**
2
+ * One entity instance on its durable object (a Cloudflare Durable Object, a
3
+ * Rivet actor), run the way Effect Cluster's runners run one
4
+ * (`effect/cluster`'s entity manager):
5
+ *
6
+ * - The instance's handlers run behind Effect's own `RpcServer`, which
7
+ * applies the RPCs' middleware, the entity's `concurrency`, `Rpc.fork`
8
+ * and `Rpc.uninterruptible`.
9
+ * - A persisted message is stored before it runs and its reply after: a
10
+ * primary key runs once, a duplicate waits for the first run's reply, and
11
+ * a message cut short by a restart runs again when the object next wakes.
12
+ * - A message with `DeliverAt` waits in storage until its time, which the
13
+ * object's alarm wakes it for.
14
+ * - An instance holds `mailboxCapacity` messages. A volatile message past
15
+ * that fails with `MailboxFull`; a persisted one waits in storage and runs
16
+ * as the instance finishes others, in the order they were stored.
17
+ * - A defect restarts the instance, backing off, and runs its messages
18
+ * again, unless `disableFatalDefects` returns it to the caller.
19
+ * - The instance stops once idle for `maxIdleTime`, closing its scope,
20
+ * unless `Entity.keepAlive` holds it; a held instance is started again
21
+ * after its object restarts.
22
+ *
23
+ * A host evicts an idle object, and the instance's memory with it, so the
24
+ * object's alarm keeps it awake while its instance runs.
25
+ *
26
+ * The host supplies the object's SQLite and alarm as an `EntityStorage`.
27
+ * Its statements may be asynchronous, so every decision about the mailbox
28
+ * runs under one lock: no two events see it half-updated.
29
+ *
30
+ * @internal
31
+ */
32
+ import * as Cause from "effect/Cause";
33
+ import * as Clock from "effect/Clock";
34
+ import { ClusterSchema, DeliverAt, Envelope, Snowflake } from "effect/cluster";
35
+ import { CurrentAddress, CurrentRunnerAddress, KeepAliveHandler, Request as EntityRequest, } from "effect/cluster/Entity";
36
+ import * as Context from "effect/Context";
37
+ import * as Deferred from "effect/Deferred";
38
+ import * as Duration from "effect/Duration";
39
+ import * as Effect from "effect/Effect";
40
+ import * as Exit from "effect/Exit";
41
+ import * as Headers from "effect/http/Headers";
42
+ import * as Option from "effect/Option";
43
+ import * as PrimaryKey from "effect/PrimaryKey";
44
+ import * as Queue from "effect/Queue";
45
+ import { RpcMessage, RpcServer } from "effect/rpc";
46
+ import * as Schedule from "effect/Schedule";
47
+ import * as Schema from "effect/Schema";
48
+ import * as Scope from "effect/Scope";
49
+ import * as Semaphore from "effect/Semaphore";
50
+ import * as Stream from "effect/Stream";
51
+ import { chunkCodec, DEFECT, decode, encode, encodeExit, payloadCodec, } from "./Wire.js";
52
+ /** How often the alarm wakes the object while its instance runs or is held. */
53
+ const KEEP_AWAKE = 5_000;
54
+ /** How long a stored message waits before the alarm runs it again, after a restart. */
55
+ const REPLAY_AFTER = 30_000;
56
+ /** How long a reply without a primary key is kept, for its caller's retries. */
57
+ const KEEP_REPLIES = 60 * 60_000;
58
+ /** How long an interrupt that arrived before its call waits for it. */
59
+ const KEEP_INTERRUPTS = 60_000;
60
+ /** Effect Cluster's back-off between attempts to build an instance. */
61
+ const restartPolicy = Schedule.min([Schedule.exponential(500, 1.5), Schedule.spaced("10 seconds")]);
62
+ /** How often the alarm prunes replies older than `KEEP_REPLIES`. */
63
+ const PRUNE_EVERY = 10 * 60_000;
64
+ const rejected = (_tag, message) => ({
65
+ _tag: "Rejected",
66
+ rejection: { _tag, message },
67
+ });
68
+ const accepted = { _tag: "Accepted" };
69
+ const run = (active) => ({ _tag: "Run", active });
70
+ const waitOn = (key, reply) => ({
71
+ _tag: "Wait",
72
+ key,
73
+ reply,
74
+ });
75
+ const replied = (reply, chunks) => ({
76
+ _tag: "Replied",
77
+ reply,
78
+ chunks,
79
+ });
80
+ const replyText = (reply) => `{"exit":${reply}}`;
81
+ export const makeEntityRuntime = Effect.fn(function* (options) {
82
+ const { hosted, address, storage } = options;
83
+ const entity = hosted.entity;
84
+ const exec = storage.exec;
85
+ const mailbox = yield* Semaphore.make(1);
86
+ /** Run `effect` alone on the mailbox, to its end even if its caller stops waiting. */
87
+ const locked = (effect) => effect.pipe(mailbox.withPermits(1), Effect.uninterruptible);
88
+ const limit = options.maxEncodedBytes;
89
+ const encoder = new TextEncoder();
90
+ const tooLarge = (text) => limit !== undefined && text.length * 3 > limit && encoder.encode(text).byteLength > limit;
91
+ // One read tells an object that is already laid out; only a new or older
92
+ // one writes its schema.
93
+ const laidOut = new Set((yield* exec("SELECT name FROM sqlite_master WHERE name IN ('cluster_mailbox_pending', 'cluster_messages')")).map((row) => row.name));
94
+ if (!laidOut.has("cluster_mailbox_pending") || laidOut.has("cluster_messages")) {
95
+ const migratedAt = yield* Clock.currentTimeMillis;
96
+ yield* storage.transaction(Effect.gen(function* () {
97
+ yield* exec(`CREATE TABLE IF NOT EXISTS cluster_mailbox (
98
+ request_id TEXT PRIMARY KEY,
99
+ call TEXT NOT NULL,
100
+ keyed INTEGER NOT NULL,
101
+ deliver_at INTEGER,
102
+ exit TEXT,
103
+ created_at INTEGER NOT NULL,
104
+ completed_at INTEGER
105
+ )`);
106
+ yield* exec("CREATE TABLE IF NOT EXISTS cluster_chunks (request_id TEXT NOT NULL, seq INTEGER NOT NULL, chunk TEXT NOT NULL, PRIMARY KEY (request_id, seq))");
107
+ yield* exec("CREATE TABLE IF NOT EXISTS cluster_entity (key TEXT PRIMARY KEY, value INTEGER NOT NULL)");
108
+ if (laidOut.has("cluster_messages")) {
109
+ // The mailbox's first layout: its rows move over, keeping their replies.
110
+ yield* exec(`INSERT OR IGNORE INTO cluster_mailbox (request_id, call, keyed, exit, created_at, completed_at)
111
+ SELECT request_id, call, instr(request_id, '/') > 0, exit, ?, CASE WHEN exit IS NULL THEN NULL ELSE ? END
112
+ FROM cluster_messages`, migratedAt, migratedAt);
113
+ yield* exec("DROP TABLE cluster_messages");
114
+ }
115
+ // Replies with a primary key are kept for good, so what the alarm
116
+ // looks for (messages not yet done, replies to prune) is indexed
117
+ // apart from them.
118
+ yield* exec("CREATE INDEX IF NOT EXISTS cluster_mailbox_completed ON cluster_mailbox (completed_at) WHERE keyed = 0");
119
+ yield* exec("CREATE INDEX IF NOT EXISTS cluster_mailbox_pending ON cluster_mailbox (deliver_at) WHERE exit IS NULL");
120
+ }));
121
+ }
122
+ const clock = yield* Clock.Clock;
123
+ const now = () => clock.currentTimeMillisUnsafe();
124
+ const snowflakes = yield* Snowflake.makeGenerator;
125
+ const maxIdle = Duration.toMillis(Duration.fromInputUnsafe(hosted.options.maxIdleTime ?? "1 minute"));
126
+ const capacity = hosted.options.mailboxCapacity ?? 4096;
127
+ const actives = new Map();
128
+ const byKey = new Map();
129
+ const byCaller = new Map();
130
+ /** Callers whose interrupt reached the object before their call, and when. */
131
+ const leftEarly = new Map();
132
+ /** Callers waiting on a message whose `DeliverAt` hasn't come. */
133
+ const waiters = new Map();
134
+ let nextId = 0;
135
+ let instance;
136
+ let building;
137
+ let restarting = false;
138
+ let restarts = 0;
139
+ let replayed = false;
140
+ let lastActive = now();
141
+ let prunedAt = 0;
142
+ /** Stored messages are due and waiting for room on the instance. */
143
+ let backlog = false;
144
+ /** Stored messages without a reply: running, cut short, or waiting for their time or for room. */
145
+ let pending = (yield* exec("SELECT COUNT(*) AS n FROM cluster_mailbox WHERE exit IS NULL"))[0]
146
+ ?.n ?? 0;
147
+ let keepAlive = (yield* exec("SELECT value FROM cluster_entity WHERE key = 'keep_alive'"))[0]
148
+ ?.value === 1;
149
+ let armedAt = (yield* storage.getAlarm) ?? undefined;
150
+ // ------------------------------------------------------------ alarm
151
+ /** Wake the object at `at`, unless it already wakes sooner. */
152
+ const arm = (at) => Effect.suspend(() => {
153
+ if (armedAt !== undefined && armedAt > now() && armedAt <= at)
154
+ return Effect.void;
155
+ armedAt = at;
156
+ return storage.setAlarm(at);
157
+ });
158
+ /** The next time the object must wake: for its instance, a replay, or a scheduled message. */
159
+ const rearm = Effect.gen(function* () {
160
+ const at = now();
161
+ let next = Number.POSITIVE_INFINITY;
162
+ if (instance !== undefined || keepAlive)
163
+ next = at + KEEP_AWAKE;
164
+ if (pending > 0) {
165
+ // A stored message that is due but not done: its run may have been cut short.
166
+ const [due] = yield* exec("SELECT EXISTS (SELECT 1 FROM cluster_mailbox WHERE exit IS NULL AND (deliver_at IS NULL OR deliver_at <= ?)) AS n", at);
167
+ if ((due?.n ?? 0) > 0)
168
+ next = Math.min(next, at + REPLAY_AFTER);
169
+ const [scheduled] = yield* exec("SELECT MIN(deliver_at) AS at FROM cluster_mailbox WHERE exit IS NULL AND deliver_at > ?", at);
170
+ if (scheduled?.at != null)
171
+ next = Math.min(next, scheduled.at);
172
+ }
173
+ if (!Number.isFinite(next))
174
+ return;
175
+ armedAt = next;
176
+ yield* storage.setAlarm(next);
177
+ });
178
+ // ------------------------------------------------------------ instance
179
+ const forget = (active) => {
180
+ actives.delete(active.id);
181
+ if (byKey.get(active.key) === active)
182
+ byKey.delete(active.key);
183
+ for (const caller of active.callers) {
184
+ if (byCaller.get(caller) === active)
185
+ byCaller.delete(caller);
186
+ }
187
+ if (actives.size === 0)
188
+ lastActive = now();
189
+ };
190
+ /** A message finished: store its reply, if persisted, and hand it to its callers. */
191
+ const complete = (active, exit) => Effect.flatMap(encodeExit(active.rpc, exit), (encoded) => locked(Effect.gen(function* () {
192
+ if (actives.get(active.id) !== active)
193
+ return;
194
+ let reply = JSON.stringify(encoded);
195
+ if (active.persisted) {
196
+ if (tooLarge(reply)) {
197
+ reply = JSON.stringify(yield* encodeExit(active.rpc, Exit.die(`The reply to ${active.key} is over ${limit} bytes and can't be stored`)));
198
+ }
199
+ // Stored before the message is forgotten: a duplicate arriving
200
+ // now finds the run or its reply, never neither.
201
+ const stored = yield* Effect.exit(exec("UPDATE cluster_mailbox SET exit = ?, completed_at = ? WHERE request_id = ?", reply, now(), active.key));
202
+ if (Exit.isSuccess(stored))
203
+ pending--;
204
+ // Its callers still get their reply; the alarm runs the message
205
+ // again, as it does for one cut short.
206
+ else
207
+ yield* Effect.logError(`Could not store the reply to ${active.key}`, stored.cause);
208
+ }
209
+ forget(active);
210
+ if (Exit.isSuccess(exit))
211
+ restarts = 0;
212
+ Deferred.doneUnsafe(active.reply, Exit.succeed(reply));
213
+ const waiting = waiters.get(active.key);
214
+ waiters.delete(active.key);
215
+ for (const waiter of waiting ?? [])
216
+ Deferred.doneUnsafe(waiter, Exit.succeed(reply));
217
+ }))).pipe(Effect.andThen(drain));
218
+ /** Stop `active`'s handler, from apart: its own fiber may be the one asking. */
219
+ const stop = (active) => Effect.suspend(() => instance === undefined || !active.delivered
220
+ ? Effect.void
221
+ : Effect.forkDetach(instance.server.write(0, {
222
+ _tag: "Interrupt",
223
+ requestId: RpcMessage.RequestId(active.id),
224
+ interruptors: [],
225
+ })));
226
+ const chunk = (active, values) => Effect.gen(function* () {
227
+ const text = JSON.stringify(yield* Effect.forEach(values, (value) => encode(chunkCodec(active.rpc), value)));
228
+ if (!active.persisted) {
229
+ for (const listener of active.listeners)
230
+ yield* listener(text);
231
+ return;
232
+ }
233
+ // Stored under the lock, so a caller joining the stream reads this
234
+ // chunk from storage or hears it here, never both and never neither.
235
+ const stored = tooLarge(text)
236
+ ? Exit.die(`A chunk of ${active.key} is over ${limit} bytes and can't be stored`)
237
+ : yield* locked(Effect.suspend(() => exec("INSERT OR REPLACE INTO cluster_chunks (request_id, seq, chunk) VALUES (?, ?, ?)", active.key, active.sequence++, text)).pipe(Effect.map(() => [...active.listeners]), Effect.exit));
238
+ if (Exit.isFailure(stored)) {
239
+ // A stream that can't be replayed in full fails where it stands.
240
+ yield* complete(active, stored.cause.pipe(Cause.pretty, Exit.die));
241
+ return yield* stop(active);
242
+ }
243
+ for (const listener of stored.value)
244
+ yield* listener(text);
245
+ });
246
+ const onFromServer = (response) => {
247
+ switch (response._tag) {
248
+ case "Exit": {
249
+ const active = actives.get(Number(response.requestId));
250
+ if (active === undefined)
251
+ return Effect.void;
252
+ // A restart's interrupts aren't replies: the rebuilt instance runs
253
+ // the message again.
254
+ if (restarting && Exit.hasInterrupts(response.exit) && !active.interrupted) {
255
+ return Effect.void;
256
+ }
257
+ return complete(active, response.exit);
258
+ }
259
+ case "Chunk": {
260
+ const active = actives.get(Number(response.requestId));
261
+ return active === undefined ? Effect.void : chunk(active, response.values);
262
+ }
263
+ case "Defect":
264
+ return restart(Cause.die(response.defect));
265
+ case "ClientEnd":
266
+ return Effect.void;
267
+ }
268
+ };
269
+ const setKeepAlive = (enabled) => Effect.gen(function* () {
270
+ keepAlive = enabled;
271
+ yield* exec("INSERT OR REPLACE INTO cluster_entity (key, value) VALUES ('keep_alive', ?)", enabled ? 1 : 0);
272
+ lastActive = now();
273
+ if (enabled)
274
+ yield* arm(now() + KEEP_AWAKE);
275
+ });
276
+ /** Build the instance's handlers and the server they run behind, once. */
277
+ const buildOnce = Effect.gen(function* () {
278
+ const scope = yield* Scope.make();
279
+ const context = options.context.pipe(Context.add(CurrentAddress, address), Context.add(CurrentRunnerAddress, options.runnerAddress), Context.add(KeepAliveHandler, setKeepAlive), Context.add(Scope.Scope, scope));
280
+ return yield* Effect.gen(function* () {
281
+ const handlers = yield* entity.protocol.toHandlers(hosted.instance).pipe(Effect.provideContext(context));
282
+ const makeServer = RpcServer.makeNoSerialization(entity.protocol, {
283
+ onFromServer,
284
+ spanPrefix: entity.type,
285
+ spanAttributes: { "entity.type": entity.type, "entity.id": address.entityId },
286
+ concurrency: hosted.options.concurrency ?? 1,
287
+ disableFatalDefects: hosted.options.disableFatalDefects,
288
+ // The object's reply stream is the backpressure.
289
+ disableClientAcks: true,
290
+ });
291
+ // oxlint-disable-next-line effecttsgo/unsafe-effect-type-assertion -- its handlers, middleware and scope are in the context provided below
292
+ const serve = makeServer;
293
+ const server = yield* serve.pipe(Effect.provideContext(Context.merge(context, handlers)));
294
+ return { scope, server };
295
+ }).pipe(Effect.onError((cause) => Scope.close(scope, Exit.failCause(cause))));
296
+ });
297
+ /** Build the instance, retrying a defect while building as Effect Cluster does. */
298
+ const buildInstance = buildOnce.pipe(Effect.sandbox, Effect.tapError((cause) => Effect.logError("Defect building entity handlers", cause)), Effect.retry(restartPolicy), Effect.orDie);
299
+ /** The running instance, building it on its first message. */
300
+ const getInstance = Effect.suspend(() => {
301
+ if (instance !== undefined)
302
+ return Effect.succeed(instance);
303
+ if (building !== undefined)
304
+ return Deferred.await(building);
305
+ const ready = Deferred.makeUnsafe();
306
+ building = ready;
307
+ // Built apart from the caller, which may stop waiting.
308
+ return Effect.forkDetach(Effect.flatMap(buildInstance, (built) => Effect.sync(() => {
309
+ instance = built;
310
+ building = undefined;
311
+ Deferred.doneUnsafe(ready, Exit.succeed(built));
312
+ })).pipe(
313
+ // Messages stored before this object last stopped run once the
314
+ // instance is back.
315
+ Effect.andThen(Effect.suspend(() => (replayed ? Effect.void : replayStored))))).pipe(Effect.andThen(Deferred.await(ready)));
316
+ });
317
+ /** Write `active` to the instance's server, which runs its handler. */
318
+ const deliver = (built, active) => Effect.suspend(() => {
319
+ if (actives.get(active.id) !== active)
320
+ return Effect.void;
321
+ active.delivered = true;
322
+ return built.server.write(0, {
323
+ _tag: "Request",
324
+ id: RpcMessage.RequestId(active.id),
325
+ tag: active.rpc._tag,
326
+ payload: active.request,
327
+ headers: active.request.headers,
328
+ });
329
+ });
330
+ const start = (active) => Effect.andThen(arm(now() + KEEP_AWAKE), Effect.flatMap(getInstance, (built) => deliver(built, active)));
331
+ /**
332
+ * A handler died: stop the instance, back off, build it again, and run
333
+ * every message it had started again, as Effect Cluster's runners do.
334
+ */
335
+ const restart = (cause) => Effect.suspend(() => {
336
+ if (restarting)
337
+ return Effect.void;
338
+ restarting = true;
339
+ const old = instance;
340
+ instance = undefined;
341
+ const ready = Deferred.makeUnsafe();
342
+ building = ready;
343
+ // Effect Cluster's back-off: 500ms, growing by half each time, at most 10s.
344
+ const delay = Math.min(500 * 1.5 ** restarts++, 10_000);
345
+ return options.waitUntil(Effect.gen(function* () {
346
+ yield* Effect.logError("Defect in entity, restarting", cause);
347
+ if (old !== undefined)
348
+ yield* Scope.close(old.scope, Exit.void);
349
+ yield* Effect.sleep(delay);
350
+ const built = yield* buildInstance;
351
+ // The messages the old instance had started run again first; those
352
+ // still waiting for an instance deliver themselves once it's ready.
353
+ const started = [...actives.values()].filter((active) => active.delivered);
354
+ for (const active of started)
355
+ yield* deliver(built, active);
356
+ instance = built;
357
+ building = undefined;
358
+ restarting = false;
359
+ Deferred.doneUnsafe(ready, Exit.succeed(built));
360
+ }));
361
+ });
362
+ /** Stop an instance that has been idle for `maxIdleTime`, closing its scope. */
363
+ const reapIfIdle = Effect.suspend(() => {
364
+ if (instance === undefined || building !== undefined || restarting)
365
+ return Effect.void;
366
+ if (keepAlive || actives.size > 0 || now() - lastActive < maxIdle)
367
+ return Effect.void;
368
+ const old = instance;
369
+ instance = undefined;
370
+ return Scope.close(old.scope, Exit.void);
371
+ });
372
+ // ------------------------------------------------------------ mailbox
373
+ const makeActive = (key, rpc, payload, call, persisted) => {
374
+ const active = {
375
+ id: nextId++,
376
+ key,
377
+ rpc,
378
+ request: new EntityRequest(Object.assign(Envelope.makeRequest({
379
+ requestId: snowflakes.nextUnsafe(),
380
+ address,
381
+ tag: rpc._tag,
382
+ payload: payload,
383
+ headers: Headers.fromInput(call.headers),
384
+ }), { lastSentChunk: Option.none() })),
385
+ persisted,
386
+ callers: new Set(),
387
+ reply: Deferred.makeUnsafe(),
388
+ listeners: new Set(),
389
+ sequence: 0,
390
+ delivered: false,
391
+ interrupted: false,
392
+ };
393
+ actives.set(active.id, active);
394
+ byKey.set(key, active);
395
+ return active;
396
+ };
397
+ const join = (active, caller) => {
398
+ if (caller === undefined)
399
+ return;
400
+ active.callers.add(caller);
401
+ byCaller.set(caller, active);
402
+ };
403
+ const waitFor = (key) => {
404
+ const reply = Deferred.makeUnsafe();
405
+ waiters.set(key, [...(waiters.get(key) ?? []), reply]);
406
+ return waitOn(key, reply);
407
+ };
408
+ /**
409
+ * Whether the instance holds its capacity of messages, running or waiting
410
+ * for their turn to. As on Effect Cluster's runners, a stored message
411
+ * waiting for its `DeliverAt` isn't one of them.
412
+ */
413
+ const full = () => capacity !== "unbounded" && actives.size >= capacity;
414
+ /**
415
+ * A stored message that is due but has to wait: the instance is full, or
416
+ * others are waiting ahead of it.
417
+ */
418
+ const queued = (mode, key) => {
419
+ backlog = true;
420
+ return mode === "tell" ? accepted : waitFor(key);
421
+ };
422
+ const storedChunks = (key) => Effect.map(exec("SELECT chunk FROM cluster_chunks WHERE request_id = ? ORDER BY seq", key), (rows) => rows.map((row) => row.chunk));
423
+ /**
424
+ * Admit a call: run it, join the run already answering it, answer it
425
+ * from storage, or reject it. Everything after decoding runs under the
426
+ * mailbox's lock, so no other event sees the mailbox half-updated.
427
+ */
428
+ const admit = (call, mode,
429
+ /** Where a stream's chunks go, if it joins one already running. */
430
+ listen = () => Effect.void) => Effect.gen(function* () {
431
+ const rpc = entity.protocol.requests.get(call.tag);
432
+ if (rpc === undefined) {
433
+ return yield* Effect.die(new Error(`Entity '${entity.type}' has no RPC '${call.tag}'.`));
434
+ }
435
+ const payload = yield* decode(payloadCodec(rpc), call.payload);
436
+ const persisted = Context.get(rpc.annotations, ClusterSchema.Persisted);
437
+ const keyed = PrimaryKey.isPrimaryKey(payload);
438
+ const deliverAt = DeliverAt.toMillis(payload);
439
+ if (!persisted) {
440
+ if (full())
441
+ return rejected("MailboxFull", `${entity.type} mailbox is full`);
442
+ const active = makeActive(call.requestId, rpc, payload, call, false);
443
+ join(active, call.requestId);
444
+ return run(active);
445
+ }
446
+ const key = keyed ? `${rpc._tag}/${PrimaryKey.value(payload)}` : call.requestId;
447
+ return yield* locked(Effect.gen(function* () {
448
+ const running = byKey.get(key);
449
+ if (running !== undefined) {
450
+ // A duplicate waits for the first run's reply; giving up doesn't
451
+ // interrupt the run.
452
+ if (mode !== "stream")
453
+ return mode === "tell" ? accepted : waitOn(key, running.reply);
454
+ running.listeners.add(listen);
455
+ return {
456
+ _tag: "Join",
457
+ active: running,
458
+ chunks: yield* storedChunks(key),
459
+ };
460
+ }
461
+ const at = now();
462
+ const [row] = yield* exec("SELECT request_id, call, exit, deliver_at FROM cluster_mailbox WHERE request_id = ?", key);
463
+ if (row?.exit != null) {
464
+ return mode === "tell"
465
+ ? accepted
466
+ : replied(row.exit, mode === "stream" ? yield* storedChunks(key) : []);
467
+ }
468
+ if (row !== undefined) {
469
+ if (row.deliver_at != null && row.deliver_at > at) {
470
+ return mode === "tell" ? accepted : waitFor(key);
471
+ }
472
+ if (backlog || full())
473
+ return queued(mode, key);
474
+ // Stored, but cut short by a restart: run it now, as it was sent.
475
+ const stored = JSON.parse(row.call);
476
+ const decoded = Schema.decodeUnknownExit(payloadCodec(rpc))(stored.payload);
477
+ // Stored under an older schema: run the call as sent now.
478
+ const active = makeActive(key, rpc, Exit.isSuccess(decoded) ? decoded.value : payload, Exit.isSuccess(decoded) ? stored : call, true);
479
+ join(active, call.requestId);
480
+ return run(active);
481
+ }
482
+ const text = JSON.stringify(call);
483
+ if (tooLarge(text)) {
484
+ return rejected("PersistenceError", `The call ${key} is over ${limit} bytes`);
485
+ }
486
+ const stored = yield* Effect.exit(exec("INSERT INTO cluster_mailbox (request_id, call, keyed, deliver_at, created_at) VALUES (?, ?, ?, ?, ?)", key, text, keyed ? 1 : 0, deliverAt, at));
487
+ if (Exit.isFailure(stored))
488
+ return rejected("PersistenceError", Cause.pretty(stored.cause));
489
+ pending++;
490
+ if (deliverAt !== null && deliverAt > at) {
491
+ // Its time is the alarm's to keep.
492
+ yield* arm(deliverAt);
493
+ return mode === "tell" ? accepted : waitFor(key);
494
+ }
495
+ // A full instance doesn't turn a stored message away: it runs once
496
+ // there is room, after those stored before it.
497
+ if (backlog || full())
498
+ return queued(mode, key);
499
+ const active = makeActive(key, rpc, payload, call, true);
500
+ join(active, call.requestId);
501
+ return run(active);
502
+ })).pipe(Effect.tap(() => drain));
503
+ });
504
+ /**
505
+ * Run the stored messages whose time has come and that aren't running,
506
+ * oldest first, as far as the instance has room.
507
+ */
508
+ const replayStored = Effect.suspend(() => {
509
+ replayed = true;
510
+ if (pending === 0) {
511
+ backlog = false;
512
+ return Effect.void;
513
+ }
514
+ return locked(Effect.gen(function* () {
515
+ // Those running are among the oldest, so the instance's capacity and
516
+ // one more row tell whether any are left waiting.
517
+ const rows = yield* exec(`SELECT request_id, call FROM cluster_mailbox WHERE exit IS NULL AND (deliver_at IS NULL OR deliver_at <= ?) ORDER BY created_at, rowid${capacity === "unbounded" ? "" : ` LIMIT ${capacity + 1}`}`, now());
518
+ const started = [];
519
+ const dropped = [];
520
+ backlog = false;
521
+ for (const row of rows) {
522
+ if (byKey.has(row.request_id))
523
+ continue;
524
+ if (full()) {
525
+ backlog = true;
526
+ break;
527
+ }
528
+ const call = JSON.parse(row.call);
529
+ const rpc = entity.protocol.requests.get(call.tag);
530
+ const payload = rpc === undefined
531
+ ? undefined
532
+ : Schema.decodeUnknownExit(payloadCodec(rpc))(call.payload);
533
+ if (rpc === undefined || payload === undefined || Exit.isFailure(payload)) {
534
+ // The entity no longer accepts it: its RPCs changed since it was stored.
535
+ yield* exec("DELETE FROM cluster_mailbox WHERE request_id = ?", row.request_id);
536
+ pending--;
537
+ dropped.push(row.request_id);
538
+ continue;
539
+ }
540
+ started.push(makeActive(row.request_id, rpc, payload.value, call, true));
541
+ }
542
+ return { started, dropped };
543
+ })).pipe(Effect.flatMap(({ started, dropped }) => {
544
+ const logDropped = dropped.length === 0
545
+ ? Effect.void
546
+ : Effect.logError(`Dropped stored ${entity.type} messages its RPCs no longer accept`, dropped);
547
+ if (started.length === 0)
548
+ return logDropped;
549
+ return Effect.flatMap(getInstance, (built) => Effect.forEach(started, (active) => deliver(built, active), { discard: true })).pipe(Effect.andThen(options.waitUntil(Effect.forEach(started, (active) => Deferred.await(active.reply), { discard: true }))), Effect.andThen(logDropped));
550
+ }));
551
+ });
552
+ /** Run waiting stored messages, in the background, once the instance has room for one. */
553
+ const drain = Effect.suspend(() => backlog && !full() ? options.waitUntil(replayStored) : Effect.void);
554
+ /** A caller stopped waiting: interrupt its message, unless it is uninterruptible. */
555
+ const interrupt = (caller) => Effect.suspend(() => {
556
+ const active = byCaller.get(caller);
557
+ if (active === undefined) {
558
+ // Its call may still be on its way: the two reach the object apart.
559
+ const at = now();
560
+ for (const [left, since] of leftEarly) {
561
+ if (at - since < KEEP_INTERRUPTS)
562
+ break;
563
+ leftEarly.delete(left);
564
+ }
565
+ leftEarly.set(caller, at);
566
+ return Effect.void;
567
+ }
568
+ if (ClusterSchema.isUninterruptibleForServer(active.rpc.annotations))
569
+ return Effect.void;
570
+ active.interrupted = true;
571
+ if (!active.delivered || instance === undefined)
572
+ return complete(active, Exit.interrupt());
573
+ return instance.server.write(0, {
574
+ _tag: "Interrupt",
575
+ requestId: RpcMessage.RequestId(active.id),
576
+ interruptors: [],
577
+ });
578
+ });
579
+ /**
580
+ * Start a message its caller sent, unless the caller has already left:
581
+ * then the message is interrupted before it runs, as Effect Cluster's
582
+ * runners do.
583
+ */
584
+ const begin = (active, caller) => Effect.suspend(() => {
585
+ if (!leftEarly.delete(caller))
586
+ return start(active);
587
+ return Effect.andThen(interrupt(caller),
588
+ // An uninterruptible message runs whether or not anyone waits.
589
+ Effect.suspend(() => (active.interrupted ? Effect.void : start(active))));
590
+ });
591
+ const ask = (call) => Effect.gen(function* () {
592
+ const admission = yield* admit(call, "ask");
593
+ switch (admission._tag) {
594
+ case "Rejected":
595
+ return JSON.stringify({ rejected: admission.rejection });
596
+ case "Replied":
597
+ return replyText(admission.reply);
598
+ case "Accepted":
599
+ case "Join":
600
+ return yield* Effect.die(new Error("An ask was accepted without a reply"));
601
+ case "Wait":
602
+ return replyText(yield* Deferred.await(admission.reply));
603
+ case "Run":
604
+ yield* begin(admission.active, call.requestId);
605
+ return replyText(yield* Deferred.await(admission.active.reply));
606
+ }
607
+ });
608
+ /** A discarded call: answered once it is stored (or started), not once it runs. */
609
+ const tell = (call) => Effect.gen(function* () {
610
+ const admission = yield* admit(call, "tell");
611
+ if (admission._tag === "Rejected")
612
+ return JSON.stringify({ rejected: admission.rejection });
613
+ if (admission._tag === "Run") {
614
+ const active = admission.active;
615
+ yield* options.waitUntil(Effect.andThen(start(active), Deferred.await(active.reply)));
616
+ }
617
+ return "{}";
618
+ });
619
+ const stream = (call) => Stream.callback((queue) => {
620
+ const offer = (frame) => Queue.offer(queue, frame);
621
+ const emit = (chunk) => Effect.asVoid(offer(`{"chunk":${chunk}}`));
622
+ // Chunks sent while this caller is still reading the stored ones wait
623
+ // behind them.
624
+ const caughtUp = Deferred.makeUnsafe();
625
+ const listen = (chunk) => Effect.andThen(Deferred.await(caughtUp), emit(chunk));
626
+ // The run this caller is listening to, which it stops hearing when it leaves.
627
+ let heard;
628
+ return Effect.gen(function* () {
629
+ const admission = yield* admit(call, "stream", listen);
630
+ switch (admission._tag) {
631
+ case "Rejected":
632
+ yield* offer(JSON.stringify({ rejected: admission.rejection }));
633
+ break;
634
+ case "Replied":
635
+ for (const chunk of admission.chunks)
636
+ yield* offer(`{"chunk":${chunk}}`);
637
+ yield* offer(replyText(admission.reply));
638
+ break;
639
+ case "Accepted":
640
+ return yield* Effect.die(new Error("A stream was accepted without a reply"));
641
+ case "Join": {
642
+ heard = admission.active;
643
+ for (const chunk of admission.chunks)
644
+ yield* emit(chunk);
645
+ Deferred.doneUnsafe(caughtUp, Exit.void);
646
+ yield* offer(replyText(yield* Deferred.await(admission.active.reply)));
647
+ break;
648
+ }
649
+ case "Wait": {
650
+ // A stored message's reply: its chunks are read from storage once it ends.
651
+ const reply = yield* Deferred.await(admission.reply);
652
+ for (const chunk of yield* storedChunks(admission.key)) {
653
+ yield* offer(`{"chunk":${chunk}}`);
654
+ }
655
+ yield* offer(replyText(reply));
656
+ break;
657
+ }
658
+ case "Run": {
659
+ const active = admission.active;
660
+ heard = active;
661
+ Deferred.doneUnsafe(caughtUp, Exit.void);
662
+ active.listeners.add(listen);
663
+ const reply = yield* Effect.andThen(begin(active, call.requestId), Deferred.await(active.reply)).pipe(
664
+ // The caller stopped reading.
665
+ Effect.onInterrupt(() => interrupt(call.requestId)));
666
+ yield* offer(replyText(reply));
667
+ break;
668
+ }
669
+ }
670
+ yield* Queue.end(queue);
671
+ }).pipe(
672
+ // A caller that leaves mustn't hold up the stream's other readers.
673
+ Effect.ensuring(Effect.sync(() => {
674
+ Deferred.doneUnsafe(caughtUp, Exit.void);
675
+ heard?.listeners.delete(listen);
676
+ })), Effect.catchCauseIf((cause) => !Cause.hasInterruptsOnly(cause), (cause) => Effect.andThen(offer(JSON.stringify({ [DEFECT]: Cause.pretty(cause) })), Queue.end(queue))));
677
+ });
678
+ /**
679
+ * The object's alarm: start a held instance its object lost, run stored
680
+ * messages that are due, stop an idle instance, and wake again when
681
+ * anything is left.
682
+ */
683
+ const alarm = Effect.gen(function* () {
684
+ armedAt = undefined;
685
+ if (keepAlive && instance === undefined)
686
+ yield* getInstance;
687
+ yield* replayStored;
688
+ yield* reapIfIdle;
689
+ if (now() - prunedAt < PRUNE_EVERY)
690
+ return;
691
+ prunedAt = now();
692
+ const cutoff = now() - KEEP_REPLIES;
693
+ yield* exec("DELETE FROM cluster_chunks WHERE request_id IN (SELECT request_id FROM cluster_mailbox WHERE keyed = 0 AND completed_at < ?)", cutoff);
694
+ yield* exec("DELETE FROM cluster_mailbox WHERE keyed = 0 AND completed_at < ?", cutoff);
695
+ }).pipe(Effect.tapCause((cause) => Effect.logError("Entity alarm failed", cause)), Effect.ensuring(Effect.ignore(rearm)), Effect.ignore);
696
+ return { ask, tell, stream, interrupt, alarm };
697
+ });
698
+ //# sourceMappingURL=EntityRuntime.js.map