@effect/platform-cloudflare 0.0.0 → 4.1.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/AGENTS.md +434 -0
- package/CLAUDE.md +434 -0
- package/LICENSE +21 -0
- package/README.md +506 -0
- package/ai-docs/README.md +44 -0
- package/ai-docs/package.json +36 -0
- package/ai-docs/src/01_effect/01_basics/01_effect-gen.ts +30 -0
- package/ai-docs/src/01_effect/01_basics/02_effect-fn.ts +52 -0
- package/ai-docs/src/01_effect/01_basics/10_creating-effects.ts +74 -0
- package/ai-docs/src/01_effect/01_basics/index.md +7 -0
- package/ai-docs/src/01_effect/02_schema/10_schema-basics.ts +43 -0
- package/ai-docs/src/01_effect/02_schema/index.md +7 -0
- package/ai-docs/src/01_effect/03_services/01_service.ts +45 -0
- package/ai-docs/src/01_effect/03_services/10_reference.ts +10 -0
- package/ai-docs/src/01_effect/03_services/20_layer-composition.ts +70 -0
- package/ai-docs/src/01_effect/03_services/20_layer-unwrap.ts +66 -0
- package/ai-docs/src/01_effect/03_services/index.md +5 -0
- package/ai-docs/src/01_effect/04_errors/01_error-handling.ts +30 -0
- package/ai-docs/src/01_effect/04_errors/10_catch-tags.ts +24 -0
- package/ai-docs/src/01_effect/04_errors/20_reason-errors.ts +64 -0
- package/ai-docs/src/01_effect/04_errors/index.md +1 -0
- package/ai-docs/src/01_effect/05_resources/10_acquire-release.ts +105 -0
- package/ai-docs/src/01_effect/05_resources/20_layer-side-effects.ts +31 -0
- package/ai-docs/src/01_effect/05_resources/30_layer-map.ts +86 -0
- package/ai-docs/src/01_effect/05_resources/index.md +3 -0
- package/ai-docs/src/01_effect/06_running/10_run-main.ts +30 -0
- package/ai-docs/src/01_effect/06_running/20_layer-launch.ts +27 -0
- package/ai-docs/src/01_effect/06_running/index.md +1 -0
- package/ai-docs/src/01_effect/07_pubsub/10_pubsub.ts +56 -0
- package/ai-docs/src/01_effect/07_pubsub/index.md +3 -0
- package/ai-docs/src/03_stream/10_creating-streams.ts +103 -0
- package/ai-docs/src/03_stream/20_consuming-streams.ts +137 -0
- package/ai-docs/src/03_stream/30_encoding.ts +163 -0
- package/ai-docs/src/03_stream/index.md +4 -0
- package/ai-docs/src/04_integration/10_managed-runtime.ts +129 -0
- package/ai-docs/src/04_integration/index.md +5 -0
- package/ai-docs/src/05_batching/10_request-resolver.ts +89 -0
- package/ai-docs/src/05_batching/index.md +3 -0
- package/ai-docs/src/06_schedule/10_schedules.ts +111 -0
- package/ai-docs/src/06_schedule/index.md +3 -0
- package/ai-docs/src/07_datetime/10_creating-and-formatting.ts +30 -0
- package/ai-docs/src/07_datetime/20_time-zones.ts +44 -0
- package/ai-docs/src/07_datetime/index.md +5 -0
- package/ai-docs/src/08_observability/10_logging.ts +66 -0
- package/ai-docs/src/08_observability/20_otlp-tracing.ts +95 -0
- package/ai-docs/src/08_observability/index.md +7 -0
- package/ai-docs/src/09_testing/10_effect-tests.ts +55 -0
- package/ai-docs/src/09_testing/20_layer-tests.ts +138 -0
- package/ai-docs/src/09_testing/index.md +1 -0
- package/ai-docs/src/10_predicate/01_basics.ts +14 -0
- package/ai-docs/src/10_predicate/index.md +9 -0
- package/ai-docs/src/40_sql/10_basics.ts +175 -0
- package/ai-docs/src/40_sql/index.md +6 -0
- package/ai-docs/src/50_http-client/10_basics.ts +102 -0
- package/ai-docs/src/50_http-client/index.md +3 -0
- package/ai-docs/src/51_http-server/10_basics.ts +118 -0
- package/ai-docs/src/51_http-server/20_testing.ts +100 -0
- package/ai-docs/src/51_http-server/fixtures/api/Api.ts +14 -0
- package/ai-docs/src/51_http-server/fixtures/api/Authorization.ts +36 -0
- package/ai-docs/src/51_http-server/fixtures/api/System.ts +10 -0
- package/ai-docs/src/51_http-server/fixtures/api/Users.ts +107 -0
- package/ai-docs/src/51_http-server/fixtures/domain/User.ts +21 -0
- package/ai-docs/src/51_http-server/fixtures/domain/UserErrors.ts +22 -0
- package/ai-docs/src/51_http-server/fixtures/server/Authorization.ts +37 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users/http.ts +80 -0
- package/ai-docs/src/51_http-server/fixtures/server/Users.ts +183 -0
- package/ai-docs/src/51_http-server/index.md +3 -0
- package/ai-docs/src/52_graphql/10_basics.ts +117 -0
- package/ai-docs/src/52_graphql/20_testing.ts +45 -0
- package/ai-docs/src/52_graphql/30_batching.ts +118 -0
- package/ai-docs/src/52_graphql/40_aliased-batching.ts +124 -0
- package/ai-docs/src/52_graphql/50_errors-and-retries.ts +74 -0
- package/ai-docs/src/52_graphql/index.md +6 -0
- package/ai-docs/src/60_child-process/10_working-with-child-processes.ts +117 -0
- package/ai-docs/src/60_child-process/index.md +3 -0
- package/ai-docs/src/70_cli/10_basics.ts +162 -0
- package/ai-docs/src/70_cli/index.md +5 -0
- package/ai-docs/src/71_ai/10_language-model.ts +156 -0
- package/ai-docs/src/71_ai/20_tools.ts +226 -0
- package/ai-docs/src/71_ai/30_chat.ts +158 -0
- package/ai-docs/src/71_ai/fixtures/domain/LaunchPlan.ts +9 -0
- package/ai-docs/src/71_ai/index.md +5 -0
- package/ai-docs/src/80_cluster/10_entities.ts +97 -0
- package/ai-docs/src/80_cluster/index.md +4 -0
- package/ai-docs/src/index.md +10 -0
- package/ai-docs/tsconfig.json +28 -0
- package/dist/AlchemyCloudflareCluster.d.ts +134 -0
- package/dist/AlchemyCloudflareCluster.d.ts.map +1 -0
- package/dist/AlchemyCloudflareCluster.js +111 -0
- package/dist/AlchemyCloudflareCluster.js.map +1 -0
- package/dist/CloudflareCluster.d.ts +196 -0
- package/dist/CloudflareCluster.d.ts.map +1 -0
- package/dist/CloudflareCluster.js +533 -0
- package/dist/CloudflareCluster.js.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts +279 -0
- package/dist/CloudflareDurableObjectPrograms.d.ts.map +1 -0
- package/dist/CloudflareDurableObjectPrograms.js +295 -0
- package/dist/CloudflareDurableObjectPrograms.js.map +1 -0
- package/dist/CloudflareDurableObjects.d.ts +149 -0
- package/dist/CloudflareDurableObjects.d.ts.map +1 -0
- package/dist/CloudflareDurableObjects.js +337 -0
- package/dist/CloudflareDurableObjects.js.map +1 -0
- package/dist/CloudflarePersistedQueue.d.ts +36 -0
- package/dist/CloudflarePersistedQueue.d.ts.map +1 -0
- package/dist/CloudflarePersistedQueue.js +122 -0
- package/dist/CloudflarePersistedQueue.js.map +1 -0
- package/dist/CloudflareWorkflowEngine.d.ts +72 -0
- package/dist/CloudflareWorkflowEngine.d.ts.map +1 -0
- package/dist/CloudflareWorkflowEngine.js +187 -0
- package/dist/CloudflareWorkflowEngine.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +33 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/alchemyCluster.d.ts +2 -0
- package/dist/internal/alchemyCluster.d.ts.map +1 -0
- package/dist/internal/alchemyCluster.js +75 -0
- package/dist/internal/alchemyCluster.js.map +1 -0
- package/dist/internal/boundedMap.d.ts +3 -0
- package/dist/internal/boundedMap.d.ts.map +1 -0
- package/dist/internal/boundedMap.js +15 -0
- package/dist/internal/boundedMap.js.map +1 -0
- package/dist/internal/cluster.d.ts +57 -0
- package/dist/internal/cluster.d.ts.map +1 -0
- package/dist/internal/cluster.js +2 -0
- package/dist/internal/cluster.js.map +1 -0
- package/dist/internal/clusterName.d.ts +2 -0
- package/dist/internal/clusterName.d.ts.map +1 -0
- package/dist/internal/clusterName.js +16 -0
- package/dist/internal/clusterName.js.map +1 -0
- package/dist/internal/entityKeepAlive.d.ts +2 -0
- package/dist/internal/entityKeepAlive.d.ts.map +1 -0
- package/dist/internal/entityKeepAlive.js +73 -0
- package/dist/internal/entityKeepAlive.js.map +1 -0
- package/dist/internal/entityMailbox.d.ts +2 -0
- package/dist/internal/entityMailbox.d.ts.map +1 -0
- package/dist/internal/entityMailbox.js +160 -0
- package/dist/internal/entityMailbox.js.map +1 -0
- package/dist/internal/entityRegistry.d.ts +16 -0
- package/dist/internal/entityRegistry.d.ts.map +1 -0
- package/dist/internal/entityRegistry.js +22 -0
- package/dist/internal/entityRegistry.js.map +1 -0
- package/dist/internal/entityReply.d.ts +2 -0
- package/dist/internal/entityReply.d.ts.map +1 -0
- package/dist/internal/entityReply.js +35 -0
- package/dist/internal/entityReply.js.map +1 -0
- package/dist/internal/entityRuntime.d.ts +2 -0
- package/dist/internal/entityRuntime.d.ts.map +1 -0
- package/dist/internal/entityRuntime.js +823 -0
- package/dist/internal/entityRuntime.js.map +1 -0
- package/dist/internal/entitySql.d.ts +2 -0
- package/dist/internal/entitySql.d.ts.map +1 -0
- package/dist/internal/entitySql.js +29 -0
- package/dist/internal/entitySql.js.map +1 -0
- package/dist/internal/entityStorage.d.ts +2 -0
- package/dist/internal/entityStorage.d.ts.map +1 -0
- package/dist/internal/entityStorage.js +98 -0
- package/dist/internal/entityStorage.js.map +1 -0
- package/dist/internal/entityWire.d.ts +2 -0
- package/dist/internal/entityWire.d.ts.map +1 -0
- package/dist/internal/entityWire.js +146 -0
- package/dist/internal/entityWire.js.map +1 -0
- package/dist/internal/queueRuntime.d.ts +2 -0
- package/dist/internal/queueRuntime.d.ts.map +1 -0
- package/dist/internal/queueRuntime.js +88 -0
- package/dist/internal/queueRuntime.js.map +1 -0
- package/dist/internal/queueStorage.d.ts +2 -0
- package/dist/internal/queueStorage.d.ts.map +1 -0
- package/dist/internal/queueStorage.js +129 -0
- package/dist/internal/queueStorage.js.map +1 -0
- package/dist/internal/registry.d.ts +10 -0
- package/dist/internal/registry.d.ts.map +1 -0
- package/dist/internal/registry.js +24 -0
- package/dist/internal/registry.js.map +1 -0
- package/dist/internal/singletonRegistry.d.ts +2 -0
- package/dist/internal/singletonRegistry.d.ts.map +1 -0
- package/dist/internal/singletonRegistry.js +9 -0
- package/dist/internal/singletonRegistry.js.map +1 -0
- package/dist/internal/singletonRuntime.d.ts +2 -0
- package/dist/internal/singletonRuntime.d.ts.map +1 -0
- package/dist/internal/singletonRuntime.js +42 -0
- package/dist/internal/singletonRuntime.js.map +1 -0
- package/dist/internal/singletonStorage.d.ts +2 -0
- package/dist/internal/singletonStorage.d.ts.map +1 -0
- package/dist/internal/singletonStorage.js +34 -0
- package/dist/internal/singletonStorage.js.map +1 -0
- package/dist/internal/transport.d.ts +2 -0
- package/dist/internal/transport.d.ts.map +1 -0
- package/dist/internal/transport.js +49 -0
- package/dist/internal/transport.js.map +1 -0
- package/dist/internal/workflowRegistry.d.ts +2 -0
- package/dist/internal/workflowRegistry.d.ts.map +1 -0
- package/dist/internal/workflowRegistry.js +29 -0
- package/dist/internal/workflowRegistry.js.map +1 -0
- package/dist/internal/workflowRuntime.d.ts +2 -0
- package/dist/internal/workflowRuntime.d.ts.map +1 -0
- package/dist/internal/workflowRuntime.js +322 -0
- package/dist/internal/workflowRuntime.js.map +1 -0
- package/dist/internal/workflowStorage.d.ts +2 -0
- package/dist/internal/workflowStorage.d.ts.map +1 -0
- package/dist/internal/workflowStorage.js +113 -0
- package/dist/internal/workflowStorage.js.map +1 -0
- package/dist/internal/workflowWire.d.ts +2 -0
- package/dist/internal/workflowWire.d.ts.map +1 -0
- package/dist/internal/workflowWire.js +52 -0
- package/dist/internal/workflowWire.js.map +1 -0
- package/package.json +77 -1
- package/src/AlchemyCloudflareCluster.ts +200 -0
- package/src/CloudflareCluster.ts +704 -0
- package/src/CloudflareDurableObjectPrograms.ts +543 -0
- package/src/CloudflareDurableObjects.ts +430 -0
- package/src/CloudflarePersistedQueue.ts +189 -0
- package/src/CloudflareWorkflowEngine.ts +220 -0
- package/src/index.ts +38 -0
- package/src/internal/alchemyCluster.ts +120 -0
- package/src/internal/boundedMap.ts +15 -0
- package/src/internal/cluster.ts +57 -0
- package/src/internal/clusterName.ts +19 -0
- package/src/internal/entityKeepAlive.ts +121 -0
- package/src/internal/entityMailbox.ts +324 -0
- package/src/internal/entityRegistry.ts +46 -0
- package/src/internal/entityReply.ts +54 -0
- package/src/internal/entityRuntime.ts +1318 -0
- package/src/internal/entitySql.ts +60 -0
- package/src/internal/entityStorage.ts +168 -0
- package/src/internal/entityWire.ts +204 -0
- package/src/internal/queueRuntime.ts +174 -0
- package/src/internal/queueStorage.ts +255 -0
- package/src/internal/registry.ts +31 -0
- package/src/internal/singletonRegistry.ts +21 -0
- package/src/internal/singletonRuntime.ts +88 -0
- package/src/internal/singletonStorage.ts +64 -0
- package/src/internal/transport.ts +67 -0
- package/src/internal/workflowRegistry.ts +78 -0
- package/src/internal/workflowRuntime.ts +443 -0
- package/src/internal/workflowStorage.ts +234 -0
- package/src/internal/workflowWire.ts +123 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The entity Durable Object's SQLite exposed to entity handlers as
|
|
3
|
+
* `DurableObjectSqlClient`.
|
|
4
|
+
*
|
|
5
|
+
* A user transaction holds `storage.transaction()` open across Effect
|
|
6
|
+
* suspensions, and every write on the object's single SQLite connection lands
|
|
7
|
+
* inside it. Another handler resumed from inside the transaction could save
|
|
8
|
+
* its reply there and lose it to the user's rollback, so mailbox writes wait
|
|
9
|
+
* on the same gate as user transactions.
|
|
10
|
+
*
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
import type { DurableObjectStorage } from "@cloudflare/workers-types"
|
|
14
|
+
import * as SqliteClient from "@effect/sql-sqlite-do/SqliteClient"
|
|
15
|
+
import * as Context from "effect/Context"
|
|
16
|
+
import * as Effect from "effect/Effect"
|
|
17
|
+
import * as Reactivity from "effect/reactivity/Reactivity"
|
|
18
|
+
import type * as Scope from "effect/Scope"
|
|
19
|
+
import * as Semaphore from "effect/Semaphore"
|
|
20
|
+
import type * as SqlClient from "effect/sql/SqlClient"
|
|
21
|
+
import { DurableObjectSqlClient } from "../CloudflareCluster.ts"
|
|
22
|
+
|
|
23
|
+
/** @internal */
|
|
24
|
+
export interface EntitySql {
|
|
25
|
+
/** Builds the client for one handler build, closed with that build's scope. */
|
|
26
|
+
readonly make: Effect.Effect<Context.Context<DurableObjectSqlClient>, never, Scope.Scope>
|
|
27
|
+
/** Runs a mailbox write once no user transaction is open. */
|
|
28
|
+
readonly guard: <A, E, R>(effect: Effect.Effect<A, E, R>) => Effect.Effect<A, E, R>
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const InUserTransaction = Context.Reference<boolean>(
|
|
32
|
+
"@effect/platform-cloudflare/internal/entitySql/InUserTransaction",
|
|
33
|
+
{ defaultValue: () => false }
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
/** @internal */
|
|
37
|
+
export const makeEntitySql = (storage: DurableObjectStorage): EntitySql => {
|
|
38
|
+
const gate = Semaphore.makeUnsafe(1)
|
|
39
|
+
|
|
40
|
+
const make = Effect.map(
|
|
41
|
+
Effect.provideServiceEffect(SqliteClient.make({ storage }), Reactivity.Reactivity, Reactivity.make),
|
|
42
|
+
(client) => {
|
|
43
|
+
// Nested transactions reuse the outer storage transaction, so only the
|
|
44
|
+
// outermost one takes the gate.
|
|
45
|
+
const withTransaction = client.withTransaction
|
|
46
|
+
const gated: SqlClient.SqlClient["withTransaction"] = (effect) =>
|
|
47
|
+
Effect.withFiber((fiber) =>
|
|
48
|
+
fiber.getRef(InUserTransaction)
|
|
49
|
+
? withTransaction(effect)
|
|
50
|
+
: Semaphore.withPermit(gate, withTransaction(Effect.provideService(effect, InUserTransaction, true)))
|
|
51
|
+
)
|
|
52
|
+
return Context.make(DurableObjectSqlClient, Object.assign(client, { withTransaction: gated }))
|
|
53
|
+
}
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
make,
|
|
58
|
+
guard: (effect) => Semaphore.withPermit(gate, effect)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Storage glue for the entity Durable Object constructor. The constructor must
|
|
3
|
+
* stay cheap: open SQLite, ensure the mailbox tables, and re-arm the single
|
|
4
|
+
* alarm. No user handlers are built here.
|
|
5
|
+
*
|
|
6
|
+
* @internal
|
|
7
|
+
*/
|
|
8
|
+
import type { DurableObjectStorage, SqlStorage } from "@cloudflare/workers-types"
|
|
9
|
+
import * as Effect from "effect/Effect"
|
|
10
|
+
import * as Result from "effect/Result"
|
|
11
|
+
|
|
12
|
+
/** @internal */
|
|
13
|
+
export type EntityAlarm = Pick<DurableObjectStorage, "getAlarm" | "setAlarm">
|
|
14
|
+
|
|
15
|
+
type DeliverAtRow = {
|
|
16
|
+
readonly deliver_at: number | null
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Runs a synchronous storage effect inside `transactionSync`. A defect throws
|
|
21
|
+
* out of the callback and rolls the transaction back; a typed failure happens
|
|
22
|
+
* before any write in the mailbox operations, so it is carried out as a plain
|
|
23
|
+
* failure.
|
|
24
|
+
*
|
|
25
|
+
* @internal
|
|
26
|
+
*/
|
|
27
|
+
export const withTransaction = <A, E>(
|
|
28
|
+
storage: Pick<DurableObjectStorage, "transactionSync">,
|
|
29
|
+
effect: Effect.Effect<A, E>
|
|
30
|
+
): Effect.Effect<A, E> =>
|
|
31
|
+
Effect.suspend(() => {
|
|
32
|
+
const result = storage.transactionSync(() => Effect.runSync(Effect.result(effect)))
|
|
33
|
+
return Result.isSuccess(result) ? Effect.succeed(result.success) : Effect.fail(result.failure)
|
|
34
|
+
})
|
|
35
|
+
|
|
36
|
+
const ddl = [
|
|
37
|
+
`CREATE TABLE IF NOT EXISTS cluster_messages (
|
|
38
|
+
request_id TEXT PRIMARY KEY,
|
|
39
|
+
message_id TEXT UNIQUE,
|
|
40
|
+
envelope TEXT NOT NULL,
|
|
41
|
+
discard INTEGER NOT NULL DEFAULT 0,
|
|
42
|
+
processed INTEGER NOT NULL DEFAULT 0,
|
|
43
|
+
last_reply_id TEXT,
|
|
44
|
+
deliver_at INTEGER,
|
|
45
|
+
reply_to TEXT
|
|
46
|
+
)`,
|
|
47
|
+
`CREATE TABLE IF NOT EXISTS cluster_replies (
|
|
48
|
+
reply_id TEXT PRIMARY KEY,
|
|
49
|
+
request_id TEXT NOT NULL,
|
|
50
|
+
reply TEXT NOT NULL,
|
|
51
|
+
kind TEXT NOT NULL,
|
|
52
|
+
sequence INTEGER,
|
|
53
|
+
acked INTEGER NOT NULL DEFAULT 0,
|
|
54
|
+
UNIQUE (request_id, sequence)
|
|
55
|
+
)`,
|
|
56
|
+
`CREATE INDEX IF NOT EXISTS cluster_messages_deliver_at_idx
|
|
57
|
+
ON cluster_messages (processed, deliver_at)`,
|
|
58
|
+
`CREATE INDEX IF NOT EXISTS cluster_replies_unacked_idx
|
|
59
|
+
ON cluster_replies (request_id) WHERE kind = 'Chunk' AND acked = 0`,
|
|
60
|
+
// One row: whether the entity wants keep-alive across restarts, and the
|
|
61
|
+
// encoded name for wakes that arrive without one.
|
|
62
|
+
`CREATE TABLE IF NOT EXISTS cluster_entity_state (
|
|
63
|
+
id INTEGER PRIMARY KEY CHECK (id = 0),
|
|
64
|
+
keep_alive INTEGER NOT NULL DEFAULT 0,
|
|
65
|
+
name TEXT
|
|
66
|
+
)`
|
|
67
|
+
]
|
|
68
|
+
|
|
69
|
+
/** @internal */
|
|
70
|
+
export const ensureEntityStorage = (sql: SqlStorage): void => {
|
|
71
|
+
for (const statement of ddl) {
|
|
72
|
+
sql.exec(statement)
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/** @internal */
|
|
77
|
+
export const earliestDeliverAt = (sql: SqlStorage): number | undefined => {
|
|
78
|
+
const rows = sql.exec<DeliverAtRow>(
|
|
79
|
+
"SELECT min(deliver_at) AS deliver_at FROM cluster_messages WHERE processed = 0 AND deliver_at IS NOT NULL"
|
|
80
|
+
).toArray()
|
|
81
|
+
return rows[0]?.deliver_at ?? undefined
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** @internal */
|
|
85
|
+
export const armAlarm = (alarm: EntityAlarm, deliverAt: number): Effect.Effect<void> =>
|
|
86
|
+
Effect.promise(() => alarm.getAlarm()).pipe(
|
|
87
|
+
Effect.flatMap((current) =>
|
|
88
|
+
current === null || current > deliverAt
|
|
89
|
+
? Effect.promise(() => alarm.setAlarm(deliverAt))
|
|
90
|
+
: Effect.void
|
|
91
|
+
)
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
type EntityStateRow = {
|
|
95
|
+
readonly keep_alive: number
|
|
96
|
+
readonly name: string | null
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** @internal */
|
|
100
|
+
export interface EntityState {
|
|
101
|
+
readonly keepAlive: boolean
|
|
102
|
+
readonly name: string | undefined
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @internal */
|
|
106
|
+
export const loadEntityState = (sql: SqlStorage): EntityState => {
|
|
107
|
+
const row = sql.exec<EntityStateRow>("SELECT keep_alive, name FROM cluster_entity_state WHERE id = 0").toArray()[0]
|
|
108
|
+
return { keepAlive: row?.keep_alive === 1, name: row?.name ?? undefined }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** @internal */
|
|
112
|
+
export const saveKeepAlive = (sql: SqlStorage, enabled: boolean): Effect.Effect<void> =>
|
|
113
|
+
Effect.sync(() => {
|
|
114
|
+
sql.exec(
|
|
115
|
+
`INSERT INTO cluster_entity_state (id, keep_alive) VALUES (0, ?)
|
|
116
|
+
ON CONFLICT (id) DO UPDATE SET keep_alive = excluded.keep_alive`,
|
|
117
|
+
enabled ? 1 : 0
|
|
118
|
+
)
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
/** @internal */
|
|
122
|
+
export const rememberEntityName = (sql: SqlStorage, name: string): void => {
|
|
123
|
+
sql.exec(
|
|
124
|
+
`INSERT INTO cluster_entity_state (id, name) VALUES (0, ?)
|
|
125
|
+
ON CONFLICT (id) DO UPDATE SET name = excluded.name`,
|
|
126
|
+
name
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
type UnprocessedRow = {
|
|
131
|
+
readonly request_id: string
|
|
132
|
+
readonly deliver_at: number | null
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* The single alarm time covering every reason to wake this entity: the
|
|
137
|
+
* earliest due row, `now` for an unprocessed row nothing is running, and a
|
|
138
|
+
* heartbeat while a row is in flight or keep-alive is wanted. An in-flight row
|
|
139
|
+
* counts as a heartbeat rather than `now`, so a long handler does not keep
|
|
140
|
+
* re-firing the alarm.
|
|
141
|
+
*
|
|
142
|
+
* @internal
|
|
143
|
+
*/
|
|
144
|
+
export const nextAlarmAt = (
|
|
145
|
+
sql: SqlStorage,
|
|
146
|
+
options: {
|
|
147
|
+
readonly now: number
|
|
148
|
+
readonly heartbeatMillis: number
|
|
149
|
+
readonly isRunning: (requestId: string) => boolean
|
|
150
|
+
}
|
|
151
|
+
): number | undefined => {
|
|
152
|
+
let next = Infinity
|
|
153
|
+
let heartbeat = loadEntityState(sql).keepAlive
|
|
154
|
+
const rows = sql.exec<UnprocessedRow>(
|
|
155
|
+
"SELECT request_id, deliver_at FROM cluster_messages WHERE processed = 0"
|
|
156
|
+
).toArray()
|
|
157
|
+
for (const row of rows) {
|
|
158
|
+
if (options.isRunning(row.request_id)) {
|
|
159
|
+
heartbeat = true
|
|
160
|
+
continue
|
|
161
|
+
}
|
|
162
|
+
next = Math.min(next, row.deliver_at === null ? options.now : Math.max(row.deliver_at, options.now))
|
|
163
|
+
}
|
|
164
|
+
if (heartbeat) {
|
|
165
|
+
next = Math.min(next, options.now + options.heartbeatMillis)
|
|
166
|
+
}
|
|
167
|
+
return Number.isFinite(next) ? next : undefined
|
|
168
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/** @internal */
|
|
2
|
+
import * as EntityAddress from "effect/cluster/EntityAddress"
|
|
3
|
+
import * as EntityId from "effect/cluster/EntityId"
|
|
4
|
+
import * as EntityType from "effect/cluster/EntityType"
|
|
5
|
+
import * as Envelope from "effect/cluster/Envelope"
|
|
6
|
+
import * as Reply from "effect/cluster/Reply"
|
|
7
|
+
import * as ShardId from "effect/cluster/ShardId"
|
|
8
|
+
import type * as Context from "effect/Context"
|
|
9
|
+
import * as Effect from "effect/Effect"
|
|
10
|
+
import * as Headers from "effect/http/Headers"
|
|
11
|
+
import * as Rpc from "effect/rpc/Rpc"
|
|
12
|
+
import * as RpcSchema from "effect/rpc/RpcSchema"
|
|
13
|
+
import * as Schema from "effect/Schema"
|
|
14
|
+
import type { EntityInvokeResult } from "../CloudflareDurableObjectPrograms.ts"
|
|
15
|
+
import type { EntityRegistration } from "./entityRegistry.ts"
|
|
16
|
+
|
|
17
|
+
type EncodedRequest = Extract<Envelope.Encoded, { readonly _tag: "Request" }>
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* The result of a `ClusterEntity.invoke` RPC. Constructed in the Durable
|
|
21
|
+
* Object as this schema's `Type` (its JSON encoding is the identity) and
|
|
22
|
+
* decoded once on the Worker side.
|
|
23
|
+
*
|
|
24
|
+
* @internal
|
|
25
|
+
*/
|
|
26
|
+
export const InvokeResult = Schema.Union([
|
|
27
|
+
Schema.Struct({
|
|
28
|
+
_tag: Schema.Literal("Success"),
|
|
29
|
+
requestId: Schema.String,
|
|
30
|
+
replies: Schema.Array(Schema.String)
|
|
31
|
+
}),
|
|
32
|
+
Schema.Struct({ _tag: Schema.Literal("MailboxFull") }),
|
|
33
|
+
Schema.Struct({ _tag: Schema.Literal("EncodedMessageTooLarge") }),
|
|
34
|
+
Schema.Struct({ _tag: Schema.Literal("AskDeduplicatedToTell") })
|
|
35
|
+
]) satisfies Schema.Codec<EntityInvokeResult>
|
|
36
|
+
|
|
37
|
+
/** @internal */
|
|
38
|
+
export const decodeInvokeResult = (value: unknown): Effect.Effect<EntityInvokeResult> =>
|
|
39
|
+
Effect.orDie(Schema.decodeUnknownEffect(InvokeResult)(value))
|
|
40
|
+
|
|
41
|
+
const EnvelopePeek = Schema.Struct({ tag: Schema.optional(Schema.String) })
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Reads the RPC tag out of a stored envelope without decoding the payload,
|
|
45
|
+
* for rows whose full decode already failed.
|
|
46
|
+
*
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
export const peekEnvelopeTag = (envelopeText: string): Effect.Effect<string | undefined> =>
|
|
50
|
+
Schema.decodeUnknownEffect(EnvelopePeek)(JSON.parse(envelopeText)).pipe(
|
|
51
|
+
Effect.match({ onFailure: () => undefined, onSuccess: ({ tag }) => tag })
|
|
52
|
+
)
|
|
53
|
+
|
|
54
|
+
/** @internal */
|
|
55
|
+
export const runWith = <A>(
|
|
56
|
+
effect: Effect.Effect<A, any, any>,
|
|
57
|
+
context: Context.Context<never>
|
|
58
|
+
): Effect.Effect<A> => effect.pipe(Effect.provideContext(context as any), Effect.orDie) as Effect.Effect<A>
|
|
59
|
+
|
|
60
|
+
// Cached per rpc so the derived AST stays stable and the memoized parser
|
|
61
|
+
// compiler can hit on repeated chunks.
|
|
62
|
+
const chunkValuesCache = new WeakMap<Rpc.AnyWithProps, Schema.Top | undefined>()
|
|
63
|
+
|
|
64
|
+
const chunkValuesCodec = (rpc: Rpc.AnyWithProps): Schema.Top | undefined => {
|
|
65
|
+
if (chunkValuesCache.has(rpc)) return chunkValuesCache.get(rpc)
|
|
66
|
+
const codec = RpcSchema.isStreamSchema(rpc.successSchema)
|
|
67
|
+
? Schema.toCodecJson(Schema.NonEmptyArray(rpc.successSchema.success))
|
|
68
|
+
: undefined
|
|
69
|
+
chunkValuesCache.set(rpc, codec)
|
|
70
|
+
return codec
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** @internal */
|
|
74
|
+
export const encodeRequest = (options: {
|
|
75
|
+
readonly requestId: string
|
|
76
|
+
readonly address: EntityAddress.EntityAddress
|
|
77
|
+
readonly tag: string
|
|
78
|
+
readonly payload: unknown
|
|
79
|
+
readonly headers: unknown
|
|
80
|
+
readonly traceId?: string | undefined
|
|
81
|
+
readonly spanId?: string | undefined
|
|
82
|
+
readonly sampled?: boolean | undefined
|
|
83
|
+
}): string =>
|
|
84
|
+
JSON.stringify({
|
|
85
|
+
_tag: "Request",
|
|
86
|
+
requestId: options.requestId,
|
|
87
|
+
address: {
|
|
88
|
+
shardId: options.address.shardId,
|
|
89
|
+
entityType: options.address.entityType,
|
|
90
|
+
entityId: options.address.entityId
|
|
91
|
+
},
|
|
92
|
+
tag: options.tag,
|
|
93
|
+
payload: options.payload,
|
|
94
|
+
headers: options.headers,
|
|
95
|
+
...(options.traceId === undefined ? undefined : {
|
|
96
|
+
traceId: options.traceId,
|
|
97
|
+
spanId: options.spanId,
|
|
98
|
+
sampled: options.sampled
|
|
99
|
+
})
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
/** @internal */
|
|
103
|
+
export const decodeRequest = (
|
|
104
|
+
registration: EntityRegistration,
|
|
105
|
+
envelopeText: string
|
|
106
|
+
): Effect.Effect<Envelope.Request.Any> =>
|
|
107
|
+
runWith(
|
|
108
|
+
Effect.gen(function*() {
|
|
109
|
+
const encoded = JSON.parse(envelopeText) as EncodedRequest
|
|
110
|
+
if (encoded._tag !== "Request" || typeof encoded.requestId !== "string") {
|
|
111
|
+
return yield* Effect.die("Expected an encoded Request envelope")
|
|
112
|
+
}
|
|
113
|
+
const rpc = registration.entity.protocol.requests.get(encoded.tag) as Rpc.AnyWithProps | undefined
|
|
114
|
+
if (rpc === undefined) return yield* Effect.die(`Unknown entity RPC tag: ${encoded.tag}`)
|
|
115
|
+
const payload = yield* Schema.decodeUnknownEffect(Schema.toCodecJson(rpc.payloadSchema))(encoded.payload)
|
|
116
|
+
return Envelope.makeRequest({
|
|
117
|
+
requestId: encoded.requestId as any,
|
|
118
|
+
address: EntityAddress.make({
|
|
119
|
+
shardId: ShardId.make(encoded.address.shardId.group, encoded.address.shardId.id),
|
|
120
|
+
entityType: EntityType.make(encoded.address.entityType),
|
|
121
|
+
entityId: EntityId.make(encoded.address.entityId)
|
|
122
|
+
}),
|
|
123
|
+
tag: encoded.tag,
|
|
124
|
+
payload,
|
|
125
|
+
headers: Headers.fromInput(encoded.headers),
|
|
126
|
+
...(encoded.traceId === undefined ? undefined : {
|
|
127
|
+
traceId: encoded.traceId,
|
|
128
|
+
spanId: encoded.spanId!,
|
|
129
|
+
sampled: encoded.sampled!
|
|
130
|
+
})
|
|
131
|
+
}) as Envelope.Request.Any
|
|
132
|
+
}),
|
|
133
|
+
registration.context
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
/** @internal */
|
|
137
|
+
export const encodeReplyFor = (
|
|
138
|
+
registration: EntityRegistration,
|
|
139
|
+
rpc: Rpc.AnyWithProps,
|
|
140
|
+
reply: Reply.Reply<any>
|
|
141
|
+
): Effect.Effect<string> => {
|
|
142
|
+
if (reply._tag === "WithExit") {
|
|
143
|
+
return runWith(
|
|
144
|
+
Effect.map(
|
|
145
|
+
Schema.encodeUnknownEffect(Schema.toCodecJson(Rpc.exitSchema(rpc)))(reply.exit),
|
|
146
|
+
(exit) => JSON.stringify({ _tag: "WithExit", requestId: String(reply.requestId), id: String(reply.id), exit })
|
|
147
|
+
),
|
|
148
|
+
registration.context
|
|
149
|
+
)
|
|
150
|
+
}
|
|
151
|
+
const codec = chunkValuesCodec(rpc)
|
|
152
|
+
if (codec === undefined) {
|
|
153
|
+
return Effect.die(`Expected a stream RPC: ${rpc._tag}`)
|
|
154
|
+
}
|
|
155
|
+
return runWith(
|
|
156
|
+
Effect.map(
|
|
157
|
+
Schema.encodeUnknownEffect(codec)(reply.values),
|
|
158
|
+
(values) =>
|
|
159
|
+
JSON.stringify({
|
|
160
|
+
_tag: "Chunk" as const,
|
|
161
|
+
requestId: String(reply.requestId),
|
|
162
|
+
id: String(reply.id),
|
|
163
|
+
sequence: reply.sequence,
|
|
164
|
+
values
|
|
165
|
+
})
|
|
166
|
+
),
|
|
167
|
+
registration.context
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/** @internal */
|
|
172
|
+
export const decodeReplyFor = (
|
|
173
|
+
rpc: Rpc.AnyWithProps,
|
|
174
|
+
context: Context.Context<never>,
|
|
175
|
+
replyText: string
|
|
176
|
+
): Effect.Effect<Reply.Reply<any>> => {
|
|
177
|
+
const encoded = JSON.parse(replyText) as Reply.Encoded
|
|
178
|
+
if (encoded._tag === "WithExit") {
|
|
179
|
+
return runWith(
|
|
180
|
+
Effect.map(
|
|
181
|
+
Schema.decodeUnknownEffect(Schema.toCodecJson(Rpc.exitSchema(rpc)))(encoded.exit),
|
|
182
|
+
(exit) => new Reply.WithExit({ requestId: encoded.requestId as any, id: encoded.id as any, exit: exit as any })
|
|
183
|
+
),
|
|
184
|
+
context
|
|
185
|
+
)
|
|
186
|
+
}
|
|
187
|
+
const codec = chunkValuesCodec(rpc)
|
|
188
|
+
if (codec === undefined) {
|
|
189
|
+
return Effect.die(`Expected a stream RPC: ${rpc._tag}`)
|
|
190
|
+
}
|
|
191
|
+
return runWith(
|
|
192
|
+
Effect.map(
|
|
193
|
+
Schema.decodeUnknownEffect(codec)(encoded.values),
|
|
194
|
+
(values) =>
|
|
195
|
+
new Reply.Chunk({
|
|
196
|
+
requestId: encoded.requestId as any,
|
|
197
|
+
id: encoded.id as any,
|
|
198
|
+
sequence: encoded.sequence,
|
|
199
|
+
values: values as any
|
|
200
|
+
})
|
|
201
|
+
),
|
|
202
|
+
context
|
|
203
|
+
)
|
|
204
|
+
}
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The durable queue Durable Object runtime. One object holds one named queue:
|
|
3
|
+
* items live in SQLite, and every lease arms the single alarm at its expiry so
|
|
4
|
+
* an item whose worker died is redelivered once the watchdog fires. Takers
|
|
5
|
+
* with no available item wait in memory until an offer, a retry, or a lease
|
|
6
|
+
* expiry wakes them; a crash or hibernation drops those waiters, whose broken
|
|
7
|
+
* RPCs are retried from the Worker side. An interrupted taker cancels its wait
|
|
8
|
+
* by taker id, releasing an item that was already leased to it.
|
|
9
|
+
*
|
|
10
|
+
* `take` is idempotent per taker id: a retried take replaces the taker's
|
|
11
|
+
* waiter, or returns the item still leased to it. Leases are found by owner in
|
|
12
|
+
* SQLite, so a retry or cancel still finds its item after a restart.
|
|
13
|
+
*
|
|
14
|
+
* @internal
|
|
15
|
+
*/
|
|
16
|
+
import type { SqlStorage } from "@cloudflare/workers-types"
|
|
17
|
+
import * as Effect from "effect/Effect"
|
|
18
|
+
import { armAlarm, type EntityAlarm } from "./entityStorage.ts"
|
|
19
|
+
import {
|
|
20
|
+
completeItem,
|
|
21
|
+
earliestLeaseExpiry,
|
|
22
|
+
ensureQueueStorage,
|
|
23
|
+
expireLeases,
|
|
24
|
+
extendLease,
|
|
25
|
+
failItem,
|
|
26
|
+
leasedItem,
|
|
27
|
+
leaseNextItem,
|
|
28
|
+
offerItem,
|
|
29
|
+
type QueueItem,
|
|
30
|
+
releaseItem,
|
|
31
|
+
releaseOwnedItem
|
|
32
|
+
} from "./queueStorage.ts"
|
|
33
|
+
|
|
34
|
+
/** @internal */
|
|
35
|
+
export interface QueueRuntimeOptions {
|
|
36
|
+
readonly sql: SqlStorage
|
|
37
|
+
readonly alarm: EntityAlarm
|
|
38
|
+
readonly now: () => number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** @internal */
|
|
42
|
+
export interface QueueRuntime {
|
|
43
|
+
readonly offer: (
|
|
44
|
+
id: string,
|
|
45
|
+
element: string,
|
|
46
|
+
delayMillis?: number,
|
|
47
|
+
deduplicate?: "untilExecuted" | "untilRemoved"
|
|
48
|
+
) => Promise<void>
|
|
49
|
+
readonly take: (takerId: string, maxAttempts: number, leaseMillis: number) => Promise<QueueItem>
|
|
50
|
+
readonly cancelTake: (takerId: string) => Promise<void>
|
|
51
|
+
readonly complete: (id: string, takerId: string) => Promise<void>
|
|
52
|
+
readonly fail: (
|
|
53
|
+
id: string,
|
|
54
|
+
lastFailure: string,
|
|
55
|
+
takerId: string,
|
|
56
|
+
terminal?: boolean,
|
|
57
|
+
retryDelayMillis?: number
|
|
58
|
+
) => Promise<void>
|
|
59
|
+
readonly release: (id: string, takerId: string) => Promise<void>
|
|
60
|
+
readonly extend: (id: string, leaseMillis: number, takerId: string) => Promise<void>
|
|
61
|
+
readonly runAlarm: () => Promise<void>
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
interface Waiter {
|
|
65
|
+
readonly takerId: string
|
|
66
|
+
readonly maxAttempts: number
|
|
67
|
+
readonly leaseMillis: number
|
|
68
|
+
resolve: (item: QueueItem) => void
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** @internal */
|
|
72
|
+
export const makeQueueRuntime = (options: QueueRuntimeOptions): QueueRuntime => {
|
|
73
|
+
const sql = options.sql
|
|
74
|
+
ensureQueueStorage(sql)
|
|
75
|
+
|
|
76
|
+
const waiters: Array<Waiter> = []
|
|
77
|
+
|
|
78
|
+
// Waiters differ in maxAttempts, so one waiter finding nothing does not mean
|
|
79
|
+
// a later one will; every waiter gets its own lease attempt. Leasing stays
|
|
80
|
+
// fully synchronous so concurrent wake-ups cannot interleave on the waiter
|
|
81
|
+
// list; only the final alarm arm is asynchronous.
|
|
82
|
+
const wakeWaiters = Effect.suspend(() => {
|
|
83
|
+
const now = options.now()
|
|
84
|
+
let index = 0
|
|
85
|
+
while (index < waiters.length) {
|
|
86
|
+
const waiter = waiters[index]
|
|
87
|
+
const item = leaseNextItem(sql, now, now + waiter.leaseMillis, waiter.maxAttempts, waiter.takerId)
|
|
88
|
+
if (item === undefined) {
|
|
89
|
+
index++
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
waiters.splice(index, 1)
|
|
93
|
+
waiter.resolve(item)
|
|
94
|
+
}
|
|
95
|
+
// Include initial visibility deadlines even when no taker is waiting.
|
|
96
|
+
const earliest = earliestLeaseExpiry(sql)
|
|
97
|
+
return earliest === undefined ? Effect.void : armAlarm(options.alarm, earliest)
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const mutateAndWake = (mutate: () => void): Promise<void> =>
|
|
101
|
+
Effect.runPromise(Effect.suspend(() => {
|
|
102
|
+
mutate()
|
|
103
|
+
return wakeWaiters
|
|
104
|
+
}))
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
offer: (id, element, delayMillis = 0, deduplicate) =>
|
|
108
|
+
mutateAndWake(() => {
|
|
109
|
+
offerItem(sql, id, element, delayMillis > 0 ? options.now() + delayMillis : null, deduplicate)
|
|
110
|
+
}),
|
|
111
|
+
|
|
112
|
+
// The taker joins the waiter list and the shared wake pass leases to it,
|
|
113
|
+
// so an immediate take and a woken one follow the same code path.
|
|
114
|
+
take: (takerId, maxAttempts, leaseMillis) => {
|
|
115
|
+
// A broken take RPC is retried with the same taker id. The retry must
|
|
116
|
+
// not leave a second waiter that leases an item nobody handles.
|
|
117
|
+
const leased = leasedItem(sql, takerId, options.now())
|
|
118
|
+
if (leased !== undefined) return Promise.resolve(leased)
|
|
119
|
+
const existing = waiters.find((waiter) => waiter.takerId === takerId)
|
|
120
|
+
const item = new Promise<QueueItem>((resolve) => {
|
|
121
|
+
if (existing === undefined) {
|
|
122
|
+
waiters.push({ takerId, maxAttempts, leaseMillis, resolve })
|
|
123
|
+
} else {
|
|
124
|
+
existing.resolve = resolve
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
return Effect.runPromise(wakeWaiters).then(() => item)
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
cancelTake: (takerId) => {
|
|
131
|
+
const index = waiters.findIndex((waiter) => waiter.takerId === takerId)
|
|
132
|
+
if (index >= 0) {
|
|
133
|
+
waiters.splice(index, 1)
|
|
134
|
+
return Promise.resolve()
|
|
135
|
+
}
|
|
136
|
+
// The cancel can race the delivery; release an item already leased to
|
|
137
|
+
// the taker instead of stranding it.
|
|
138
|
+
return mutateAndWake(() => {
|
|
139
|
+
releaseOwnedItem(sql, takerId, options.now())
|
|
140
|
+
})
|
|
141
|
+
},
|
|
142
|
+
|
|
143
|
+
complete: (id, takerId) => Promise.resolve(completeItem(sql, id, takerId)),
|
|
144
|
+
|
|
145
|
+
fail: (id, lastFailure, takerId, terminal = false, retryDelayMillis = 0) =>
|
|
146
|
+
mutateAndWake(() => {
|
|
147
|
+
failItem(
|
|
148
|
+
sql,
|
|
149
|
+
id,
|
|
150
|
+
lastFailure,
|
|
151
|
+
takerId,
|
|
152
|
+
terminal,
|
|
153
|
+
retryDelayMillis > 0 ? options.now() + retryDelayMillis : null
|
|
154
|
+
)
|
|
155
|
+
}),
|
|
156
|
+
|
|
157
|
+
release: (id, takerId) =>
|
|
158
|
+
mutateAndWake(() => {
|
|
159
|
+
releaseItem(sql, id, takerId)
|
|
160
|
+
}),
|
|
161
|
+
|
|
162
|
+
extend: (id, leaseMillis, takerId) => Promise.resolve(extendLease(sql, id, options.now() + leaseMillis, takerId)),
|
|
163
|
+
|
|
164
|
+
runAlarm: () =>
|
|
165
|
+
Effect.runPromise(
|
|
166
|
+
Effect.gen(function*() {
|
|
167
|
+
expireLeases(sql, options.now())
|
|
168
|
+
yield* wakeWaiters
|
|
169
|
+
const next = earliestLeaseExpiry(sql)
|
|
170
|
+
if (next !== undefined) yield* armAlarm(options.alarm, next)
|
|
171
|
+
})
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
}
|