@gjermundgaraba/effect-actions-cloudflare 0.22.0 → 0.23.0
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/dist/ActionDurableObject.d.mts +6 -0
- package/dist/ActionDurableObject.mjs +7 -7
- package/dist/alarm.mjs +5 -27
- package/dist/batches.mjs +0 -1
- package/dist/calls.mjs +0 -11
- package/dist/events.mjs +0 -15
- package/dist/frames.mjs +6 -9
- package/dist/idempotency.mjs +2 -8
- package/dist/placed.mjs +0 -4
- package/dist/sockets.d.mts +0 -4
- package/dist/sockets.mjs +7 -14
- package/dist/stub.d.mts +4 -4
- package/dist/stub.mjs +9 -8
- package/dist/subscriptions.mjs +10 -22
- package/dist/tasks.mjs +3 -8
- package/dist/webhooks.mjs +5 -21
- package/docs/ActionDurableObject-events.md +0 -20
- package/docs/ActionDurableObject-failures.md +1 -1
- package/docs/ActionDurableObject-placement.md +1 -4
- package/docs/ActionDurableObject-stub.md +3 -31
- package/docs/ActionDurableObject-testing.md +0 -5
- package/docs/ActionDurableObject.md +3 -49
- package/docs/README.md +1 -1
- package/examples/chat-rooms/README.md +2 -2
- package/examples/chat-rooms/src/Api.ts +0 -3
- package/examples/chat-rooms/src/Rpc.ts +0 -7
- package/examples/chat-rooms/src/browser.ts +2 -34
- package/examples/chat-rooms/src/chat/Chat.ts +3 -23
- package/examples/chat-rooms/src/chat/activity.ts +0 -5
- package/examples/chat-rooms/src/chat/authorize.ts +0 -3
- package/examples/chat-rooms/src/chat/events.ts +0 -12
- package/examples/chat-rooms/src/chat/handlers.ts +0 -6
- package/examples/chat-rooms/src/chat/room.ts +2 -7
- package/examples/chat-rooms/src/chat/store.ts +0 -6
- package/examples/chat-rooms/src/identity/Identity.ts +0 -6
- package/examples/chat-rooms/src/identity/members.ts +1 -3
- package/examples/chat-rooms/src/identity/verifier.ts +6 -9
- package/examples/chat-rooms/src/lobby/Calls.ts +0 -5
- package/examples/chat-rooms/src/lobby/Lobby.ts +2 -7
- package/examples/chat-rooms/src/lobby/events.ts +0 -5
- package/examples/chat-rooms/src/lobby/handlers.ts +0 -3
- package/examples/chat-rooms/src/lobby/host.ts +0 -1
- package/examples/chat-rooms/src/lobby/store.ts +2 -4
- package/examples/chat-rooms/src/platform/kv-previews.ts +6 -5
- package/examples/chat-rooms/src/platform/lobby-stubs.ts +0 -15
- package/examples/chat-rooms/src/platform/sql-store.ts +0 -11
- package/examples/chat-rooms/src/rooms-entrypoint.ts +2 -10
- package/examples/chat-rooms/src/routes.ts +0 -25
- package/examples/chat-rooms/src/worker.ts +3 -24
- package/examples/chat-rooms/test/platform/sql-store.test.ts +0 -5
- package/examples/chat-rooms/test/support/socket.ts +0 -9
- package/examples/chat-rooms/test/worker.test.ts +4 -18
- package/examples/chat-rooms/vite.config.ts +0 -5
- package/package.json +3 -3
|
@@ -194,6 +194,12 @@ export interface Handlers<S = never> {
|
|
|
194
194
|
* Guide: `docs/ActionDurableObject-own-class.md` in this package.
|
|
195
195
|
*/
|
|
196
196
|
export declare const handlers: <S = never, SE = never, SR extends State | Alarm | Tasks | Event.Events | Action.Idempotency = never, A = never, E = never, R extends Allowed<A, S> = never, AE = never, AR extends S | State | Alarm | Tasks | Event.Events | Action.Idempotency | Scope.Scope = never, CA = never, CE = never, CR extends RpcServer.Protocol | S | State | Alarm | Tasks | Event.Events | Action.Idempotency = never>(ctx: DurableObjectState, options: Options<S, SE, SR, A, E, R, AE, AR, CA, CE, CR>) => Handlers<S>;
|
|
197
|
+
/**
|
|
198
|
+
* An instance of a class `make` returns: a Durable Object with the handlers it defines, each
|
|
199
|
+
* taking the runtime's own arguments, so a class extending it overrides them as `Handlers`
|
|
200
|
+
* declares them, returning a promise, and reaches `make`'s through `super`, its `env` the one its
|
|
201
|
+
* `services` function takes.
|
|
202
|
+
*/
|
|
197
203
|
/**
|
|
198
204
|
* An instance of a class `make` returns: a Durable Object with the handlers it defines, each
|
|
199
205
|
* taking the runtime's own arguments, so a class extending it overrides them as `Handlers`
|
|
@@ -24,23 +24,23 @@ const handlersOf = (ctx, options) => {
|
|
|
24
24
|
const { waits, wakeDue } = makeWaits(schedule);
|
|
25
25
|
const tasks = makeTasks(schedule, options.tasks ?? {});
|
|
26
26
|
const build = Effect.gen(function* () {
|
|
27
|
-
const
|
|
27
|
+
const sharedMemoMap = yield* Layer.makeMemoMap;
|
|
28
28
|
const scope = yield* Scope.make();
|
|
29
29
|
return yield* Effect.gen(function* () {
|
|
30
|
-
const context = yield* Layer.buildWithMemoMap(services.pipe(Layer.provideMerge(Layer.mergeAll(Layer.succeed(Alarm)(alarmOf(schedule)), Layer.succeed(Waits)(waits), Layer.succeed(Tasks)(tasks.tasks), layerEvents, layerIdempotency(ctx))), Layer.provideMerge(state), Layer.provideMerge(Layer.merge(Layer.succeed(Scheduler.Scheduler)(scheduler), options.placed === void 0 ? Layer.empty : layerInputCheck(options.placed, ctx.id.name)))),
|
|
30
|
+
const context = yield* Layer.buildWithMemoMap(services.pipe(Layer.provideMerge(Layer.mergeAll(Layer.succeed(Alarm)(alarmOf(schedule)), Layer.succeed(Waits)(waits), Layer.succeed(Tasks)(tasks.tasks), layerEvents, layerIdempotency(ctx))), Layer.provideMerge(state), Layer.provideMerge(Layer.merge(Layer.succeed(Scheduler.Scheduler)(scheduler), options.placed === void 0 ? Layer.empty : layerInputCheck(options.placed, ctx.id.name)))), sharedMemoMap, scope);
|
|
31
31
|
const given = Layer.succeedContext(context);
|
|
32
32
|
yield* schedule.restore;
|
|
33
33
|
const call = calls === void 0 ? async () => {
|
|
34
34
|
throw new Error("This Durable Object serves no rpcs over its stub: give handlers or make a calls Layer");
|
|
35
35
|
} : yield* Effect.gen(function* () {
|
|
36
36
|
const stub = yield* Effect.provideContext(makeStubProtocol, context);
|
|
37
|
-
yield* Layer.buildWithMemoMap(calls.pipe(Layer.provide(Layer.succeed(RpcServer.Protocol)(stub.protocol)), Layer.provide(given)),
|
|
37
|
+
yield* Layer.buildWithMemoMap(calls.pipe(Layer.provide(Layer.succeed(RpcServer.Protocol)(stub.protocol)), Layer.provide(given)), sharedMemoMap, scope);
|
|
38
38
|
yield* stub.served;
|
|
39
39
|
return async (request) => await Effect.runPromise(stub.serve(request));
|
|
40
40
|
});
|
|
41
|
-
const sockets = yield* Layer.buildWithMemoMap(Sockets.layer.pipe(Layer.provide(given)),
|
|
41
|
+
const sockets = yield* Layer.buildWithMemoMap(Sockets.layer.pipe(Layer.provide(given)), sharedMemoMap, scope);
|
|
42
42
|
const serve = routes === void 0 ? async () => new Response(null, { status: 404 }) : yield* Effect.gen(function* () {
|
|
43
|
-
const routed = yield* Layer.buildWithMemoMap(routes.pipe(Layer.provideMerge(Layer.succeedContext(Context.merge(context, sockets))), Layer.provide(HttpServer.layerServices), Layer.provideMerge(HttpRouter.layer)),
|
|
43
|
+
const routed = yield* Layer.buildWithMemoMap(routes.pipe(Layer.provideMerge(Layer.succeedContext(Context.merge(context, sockets))), Layer.provide(HttpServer.layerServices), Layer.provideMerge(HttpRouter.layer)), sharedMemoMap, scope);
|
|
44
44
|
return yield* Effect.sync(() => HttpEffect.toWebHandlerWith(routed)(Context.get(routed, HttpRouter.HttpRouter).asHttpEffect(), options.disableLogger === true ? void 0 : HttpMiddleware.logger));
|
|
45
45
|
});
|
|
46
46
|
return {
|
|
@@ -89,8 +89,8 @@ const handlersOf = (ctx, options) => {
|
|
|
89
89
|
if (!building) start();
|
|
90
90
|
return await promise;
|
|
91
91
|
};
|
|
92
|
-
const
|
|
93
|
-
const run = async (effect) => await withBuilt(async (built) => await Effect.runPromise(
|
|
92
|
+
const providedPerCall = ({ context }, effect) => Effect.scopedWith((scope) => Effect.provideContext(effect, context.pipe(Context.add(Scope.Scope, scope), Context.add(Layer.CurrentMemoMap, Layer.makeMemoMapUnsafe()))));
|
|
93
|
+
const run = async (effect) => await withBuilt(async (built) => await Effect.runPromise(providedPerCall(built, effect)));
|
|
94
94
|
const dispatch = async (event) => {
|
|
95
95
|
await withBuilt(async ({ sockets }) => {
|
|
96
96
|
await Effect.runPromise(Effect.asVoid(event(sockets)));
|
package/dist/alarm.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { State } from "./state.mjs";
|
|
2
2
|
import { Clock, Context, DateTime, Effect, Exit, Layer, Option, Schema, Semaphore } from "effect";
|
|
3
3
|
// src/durable/alarm.ts
|
|
4
|
-
/** Where the object's alarm times are kept, one key per owner. */
|
|
5
4
|
const alarmPrefix = "effect-actions/alarm/";
|
|
6
5
|
const keyOf = (owner) => `${alarmPrefix}${owner}`;
|
|
7
6
|
const Kept = Schema.Struct({
|
|
@@ -12,15 +11,10 @@ const keptOf = Schema.decodeUnknownOption(Kept);
|
|
|
12
11
|
const backoffRetries = 6;
|
|
13
12
|
const hour = 36e5;
|
|
14
13
|
const retryDelay = (failures) => failures > backoffRetries ? hour : 2e3 * 2 ** (failures - 1);
|
|
15
|
-
/**
|
|
16
|
-
* The object's alarm times, one per owner, kept in its storage: the runtime's one alarm is set
|
|
17
|
-
* for the earliest, and `runDue` runs each owner whose time has come. `handlers` and `make` make
|
|
18
|
-
* one per instance, which its `Alarm` and its `alarm` handler share.
|
|
19
|
-
*/
|
|
20
14
|
const makeSchedule = (state) => {
|
|
21
15
|
const { storage } = state;
|
|
22
16
|
const lock = Semaphore.makeUnsafe(1);
|
|
23
|
-
const
|
|
17
|
+
const runningInThisInstance = /* @__PURE__ */ new Set();
|
|
24
18
|
const read = (owner) => Effect.map(Effect.promise(async () => await storage.get(keyOf(owner))), keptOf);
|
|
25
19
|
const times = Effect.promise(async () => await storage.list({ prefix: alarmPrefix })).pipe(Effect.map((listed) => [...listed].flatMap(([key, value]) => Option.match(keptOf(value), {
|
|
26
20
|
onNone: () => [],
|
|
@@ -41,29 +35,15 @@ const makeSchedule = (state) => {
|
|
|
41
35
|
return {
|
|
42
36
|
rearm,
|
|
43
37
|
restore: lock.withPermits(1)(Effect.flatMap(Effect.promise(async () => await storage.getAlarm()), (alarm) => alarm === null ? Effect.flatMap(times, (kept) => kept.length === 0 ? Effect.void : rearm) : Effect.void)),
|
|
44
|
-
/**
|
|
45
|
-
* Keeps `at` as the owner's time, in place of any: set as the owner's run fails, it gives way
|
|
46
|
-
* to that run's retry where the retry is earlier.
|
|
47
|
-
*/
|
|
48
38
|
set: (owner, at) => lock.withPermits(1)(Effect.flatMap(read(owner), (kept) => Option.exists(kept, (held) => held.at === at && held.failures === 0) ? Effect.void : Effect.andThen(keep(owner, {
|
|
49
39
|
at,
|
|
50
40
|
failures: 0
|
|
51
41
|
}), rearm))),
|
|
52
|
-
|
|
53
|
-
* Keeps `at` as the owner's time unless an earlier one is kept, a retry due included: the
|
|
54
|
-
* mark of the owner's run under way is no time, so a time set from the run is kept.
|
|
55
|
-
*/
|
|
56
|
-
setBy: (owner, at) => lock.withPermits(1)(Effect.flatMap(read(owner), (kept) => Option.exists(kept, (held) => held.at <= at && (held.failures === 0 || !running.has(owner))) ? Effect.void : Effect.andThen(keep(owner, {
|
|
42
|
+
setBy: (owner, at) => lock.withPermits(1)(Effect.flatMap(read(owner), (kept) => Option.exists(kept, (held) => held.at <= at && (held.failures === 0 || !runningInThisInstance.has(owner))) ? Effect.void : Effect.andThen(keep(owner, {
|
|
57
43
|
at,
|
|
58
44
|
failures: 0
|
|
59
45
|
}), rearm))),
|
|
60
46
|
clear: (owner) => lock.withPermits(1)(Effect.andThen(Effect.promise(async () => await storage.delete(keyOf(owner))), rearm)),
|
|
61
|
-
/**
|
|
62
|
-
* Runs each owner whose time has come as its turn comes, removing its time once its run
|
|
63
|
-
* succeeds unless one was set or cleared as it ran: one that fails is logged and retried, 2
|
|
64
|
-
* seconds later, doubling, six times, then hourly, unless cleared as it ran or a time set as
|
|
65
|
-
* it ran is earlier.
|
|
66
|
-
*/
|
|
67
47
|
runDue: (owners) => Effect.gen(function* () {
|
|
68
48
|
const now = yield* Clock.currentTimeMillis;
|
|
69
49
|
const kept = yield* times;
|
|
@@ -82,7 +62,7 @@ const makeSchedule = (state) => {
|
|
|
82
62
|
yield* keep(owner, mark).pipe(Effect.andThen(rearm), Effect.andThen(Effect.promise(async () => {
|
|
83
63
|
await storage.sync();
|
|
84
64
|
})));
|
|
85
|
-
|
|
65
|
+
runningInThisInstance.add(owner);
|
|
86
66
|
return {
|
|
87
67
|
...current,
|
|
88
68
|
mark,
|
|
@@ -100,7 +80,7 @@ const makeSchedule = (state) => {
|
|
|
100
80
|
if (Exit.isFailure(exit)) yield* Effect.logError(`The alarm of ${owner} failed: retried in ${retryDelay(attempt)} ms`, exit.cause);
|
|
101
81
|
const endedAt = yield* Clock.currentTimeMillis;
|
|
102
82
|
yield* lock.withPermits(1)(Effect.gen(function* () {
|
|
103
|
-
|
|
83
|
+
runningInThisInstance.delete(owner);
|
|
104
84
|
const current = yield* stored;
|
|
105
85
|
const isMark = current?.at === mark.at && current.failures === mark.failures;
|
|
106
86
|
if (Exit.isSuccess(exit)) {
|
|
@@ -115,15 +95,13 @@ const makeSchedule = (state) => {
|
|
|
115
95
|
if (isMark || retry.at < current.at) yield* keep(owner, retry);
|
|
116
96
|
}));
|
|
117
97
|
}).pipe(Effect.ensuring(Effect.sync(() => {
|
|
118
|
-
|
|
98
|
+
runningInThisInstance.delete(owner);
|
|
119
99
|
}))), { discard: true });
|
|
120
100
|
yield* lock.withPermits(1)(rearm);
|
|
121
101
|
})
|
|
122
102
|
};
|
|
123
103
|
};
|
|
124
|
-
/** The owner the application's `alarm` option runs as. */
|
|
125
104
|
const applicationOwner = "application";
|
|
126
|
-
/** The application's alarm over `schedule`, the instance's. */
|
|
127
105
|
const alarmOf = (schedule) => Alarm.of({
|
|
128
106
|
set: (at) => schedule.setBy(applicationOwner, DateTime.toEpochMillis(at)),
|
|
129
107
|
clear: schedule.clear(applicationOwner)
|
package/dist/batches.mjs
CHANGED
|
@@ -5,6 +5,5 @@ import { Array, Effect } from "effect";
|
|
|
5
5
|
* most 128 keys each, as Cloudflare's storage API documents, though local workerd takes more.
|
|
6
6
|
*/
|
|
7
7
|
const batches = (keys) => Array.chunksOf(keys, 128);
|
|
8
|
-
/** Deletes `keys` from `storage`, one batch per call. */
|
|
9
8
|
const deleted = (storage, keys) => Effect.forEach(batches(keys), (batch) => Effect.promise(async () => await storage.delete([...batch])), { discard: true });
|
|
10
9
|
export { batches, deleted };
|
package/dist/calls.mjs
CHANGED
|
@@ -2,19 +2,8 @@ import { Effect, Predicate, Queue, Schema } from "effect";
|
|
|
2
2
|
import { RpcMessage, RpcServer } from "effect/rpc";
|
|
3
3
|
import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
|
|
4
4
|
// src/durable/calls.ts
|
|
5
|
-
/**
|
|
6
|
-
* The codec of the stub's values: each schema's type to its plain form, `Schema.toCodecIso`, which
|
|
7
|
-
* Workers RPC's structured clone carries as it is, a `Date`, bytes, `undefined` and a `Map` among
|
|
8
|
-
* them, where JSON would encode them again. An `Exit`, a `Cause` and an `Option` encode to their
|
|
9
|
-
* tagged objects, a class instance to its fields.
|
|
10
|
-
*/
|
|
11
5
|
const codecFor = Schema.toCodecIso;
|
|
12
6
|
const encodeDefect = Schema.encodeSync(codecFor(Schema.Defect()));
|
|
13
|
-
/**
|
|
14
|
-
* A unary `RpcServer.Protocol` over the object's stub, passing values, modeled on Effect's HTTP
|
|
15
|
-
* protocol: one call is one client, its requests and their responses, then its end. `serve`
|
|
16
|
-
* answers one call of `effectActionsRpc`, and `served` dies unless a server runs on `protocol`.
|
|
17
|
-
*/
|
|
18
7
|
const makeStubProtocol = Effect.gen(function* () {
|
|
19
8
|
const context = yield* Effect.context();
|
|
20
9
|
let running = false;
|
package/dist/events.mjs
CHANGED
|
@@ -18,10 +18,6 @@ const positionOf = Schema.decodeUnknownOption(Position);
|
|
|
18
18
|
const storedOf = Schema.decodeUnknownOption(Stored);
|
|
19
19
|
const encodeStored = Schema.encodeSync(Stored);
|
|
20
20
|
const seqWidth = 16;
|
|
21
|
-
/**
|
|
22
|
-
* A log's storage in the object's, under `effect-actions/log/<event>/`: its position, and each
|
|
23
|
-
* entry by its sequence number, its data encoded as the event's payload JSON.
|
|
24
|
-
*/
|
|
25
21
|
const storageLog = (storage, event) => {
|
|
26
22
|
const prefix = `effect-actions/log/${event.name}/`;
|
|
27
23
|
const positionKey = `${prefix}meta`;
|
|
@@ -85,10 +81,6 @@ const epochKey = `${revisionPrefix}epoch`;
|
|
|
85
81
|
const countPrefix = `${revisionPrefix}count/`;
|
|
86
82
|
const epochOf = Schema.decodeUnknownOption(Schema.String);
|
|
87
83
|
const countOf = Schema.decodeUnknownOption(Schema.Int);
|
|
88
|
-
/**
|
|
89
|
-
* Current sources' revisions in the object's storage, under `effect-actions/revisions/`: an
|
|
90
|
-
* epoch made as the object first builds, and a counter by name.
|
|
91
|
-
*/
|
|
92
84
|
const storageRevisions = (storage) => {
|
|
93
85
|
const get = (name) => Effect.map(Effect.promise(async () => await storage.get(`${countPrefix}${name}`)), (found) => Option.getOrElse(countOf(found), () => 0));
|
|
94
86
|
return Effect.gen(function* () {
|
|
@@ -107,13 +99,6 @@ const storageRevisions = (storage) => {
|
|
|
107
99
|
};
|
|
108
100
|
});
|
|
109
101
|
};
|
|
110
|
-
/**
|
|
111
|
-
* The object's `Event.Events`, which `handlers` and `make` provide to the services, calls and
|
|
112
|
-
* routes: each log source's log kept in the object's storage, under `effect-actions/log/`, each
|
|
113
|
-
* current source's revisions beside it, so a cursor outlives an eviction, and a check or current
|
|
114
|
-
* source's subscribers read again when notified, not on its `nextPoll`, a timer that would keep the
|
|
115
|
-
* object from hibernating. Its rules are on `handlers`.
|
|
116
|
-
*/
|
|
117
102
|
const layerEvents = Layer.effect(Event.Events)(Effect.flatMap(State, ({ storage }) => Effect.flatMap(storageRevisions(storage), (revisions) => {
|
|
118
103
|
const named = /* @__PURE__ */ new Map();
|
|
119
104
|
return Event.makeEvents({
|
package/dist/frames.mjs
CHANGED
|
@@ -2,32 +2,29 @@ import { Effect, Schema } from "effect";
|
|
|
2
2
|
import { RpcSerialization } from "effect/rpc";
|
|
3
3
|
// src/durable/frames.ts
|
|
4
4
|
const socketPrefix = "effect-actions/socket#";
|
|
5
|
-
/** The tag a socket carries for its id, unique among the object's sockets. */
|
|
6
5
|
const socketTag = (id) => `${socketPrefix}${id}`;
|
|
7
|
-
/** The id a socket's tags give it, or 0, which no socket is given, for one no protocol accepted. */
|
|
8
6
|
const socketIdOf = (tags) => {
|
|
9
7
|
const tag = tags.find((found) => found.startsWith(socketPrefix));
|
|
10
8
|
return tag === void 0 ? 0 : Number(tag.slice(22));
|
|
11
9
|
};
|
|
12
|
-
/** The tag a socket carries for the `layerWebSocket` path that accepted it. */
|
|
13
10
|
const groupTag = (path) => `effect-actions/rpc:${path}`;
|
|
14
|
-
/** A request's id on the wire, a finite number or a string, which a client tells apart. */
|
|
15
11
|
const RequestId = Schema.Union([Schema.String, Schema.Finite]);
|
|
16
|
-
/** The object's JSON parser, which keeps no state between frames. */
|
|
17
12
|
const parser = RpcSerialization.json.makeUnsafe();
|
|
18
|
-
/** Closes `socket` with `code`, or with none, unless it is already closing. */
|
|
19
13
|
const closeQuietly = (socket, code, reason) => {
|
|
20
14
|
try {
|
|
21
15
|
if (code === void 0) socket.close();
|
|
22
16
|
else socket.close(code, reason);
|
|
23
|
-
} catch {
|
|
17
|
+
} catch {
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
24
20
|
};
|
|
25
|
-
/** Sends `frame` to the socket `id`, if one holds it and takes it: one the object closed does not. */
|
|
26
21
|
const sendFrame = (state, id, frame) => Effect.sync(() => {
|
|
27
22
|
const encoded = parser.encode(frame);
|
|
28
23
|
if (encoded === void 0) return;
|
|
29
24
|
try {
|
|
30
25
|
for (const socket of state.getWebSockets(socketTag(id))) socket.send(encoded);
|
|
31
|
-
} catch {
|
|
26
|
+
} catch {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
32
29
|
});
|
|
33
30
|
export { RequestId, closeQuietly, groupTag, parser, sendFrame, socketIdOf, socketTag };
|
package/dist/idempotency.mjs
CHANGED
|
@@ -2,19 +2,13 @@ import { deleted } from "./batches.mjs";
|
|
|
2
2
|
import { Clock, Effect, Layer, Option, Schema } from "effect";
|
|
3
3
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
4
4
|
// src/durable/idempotency.ts
|
|
5
|
-
/** Where the object's kept answers live, one key per action, caller's principal and idempotency key. */
|
|
6
5
|
const keptPrefix = "effect-actions/idempotency/";
|
|
7
6
|
const hour = 36e5;
|
|
8
7
|
const page = 128;
|
|
9
8
|
const keptOf = Schema.decodeUnknownOption(Action.Idempotency.Kept);
|
|
10
|
-
/**
|
|
11
|
-
* `Action.Idempotency` over the object's storage: each answer kept 24 hours, through
|
|
12
|
-
* hibernation, eviction and deployments, and deleted once its day is past as a later one is kept,
|
|
13
|
-
* at most once an hour, so an object given no further key keeps its last day's answers.
|
|
14
|
-
*/
|
|
15
9
|
const layerIdempotency = (state) => Layer.sync(Action.Idempotency)(() => {
|
|
16
10
|
let prunedAt = -Infinity;
|
|
17
|
-
const
|
|
11
|
+
const pruneHourlyOnWrite = Effect.gen(function* () {
|
|
18
12
|
const now = yield* Clock.currentTimeMillis;
|
|
19
13
|
if (now - prunedAt < hour) return;
|
|
20
14
|
prunedAt = now;
|
|
@@ -34,7 +28,7 @@ const layerIdempotency = (state) => Layer.sync(Action.Idempotency)(() => {
|
|
|
34
28
|
get: (id) => Effect.map(Effect.promise(async () => await state.storage.get(`${keptPrefix}${id}`)), (stored) => Option.getOrUndefined(keptOf(stored))),
|
|
35
29
|
set: (id, kept) => Effect.andThen(Effect.promise(async () => {
|
|
36
30
|
await state.storage.put(`${keptPrefix}${id}`, kept);
|
|
37
|
-
}),
|
|
31
|
+
}), pruneHourlyOnWrite)
|
|
38
32
|
});
|
|
39
33
|
});
|
|
40
34
|
export { layerIdempotency };
|
package/dist/placed.mjs
CHANGED
|
@@ -37,10 +37,6 @@ const reaching = (placement, namespace) => {
|
|
|
37
37
|
return HttpClient.transform(fetching, (sent) => Effect.provideService(sent, FetchHttpClient.Fetch, async (url, init) => await stub.fetch(url, init)));
|
|
38
38
|
});
|
|
39
39
|
};
|
|
40
|
-
/**
|
|
41
|
-
* The input check of an object named `name`, given `placement`: an input of one of its contracts
|
|
42
|
-
* whose key names another object is refused.
|
|
43
|
-
*/
|
|
44
40
|
const layerInputCheck = (placement, name) => {
|
|
45
41
|
const contracts = /* @__PURE__ */ new Set([...placement.binding.actions, ...placement.binding.events]);
|
|
46
42
|
return Layer.succeed(Action.InputCheck)((contract, input) => {
|
package/dist/sockets.d.mts
CHANGED
|
@@ -2,11 +2,8 @@ import { State } from "./state.mjs";
|
|
|
2
2
|
import { Context, Effect, Layer } from "effect";
|
|
3
3
|
import { HttpRouter, HttpServerResponse } from "effect/http";
|
|
4
4
|
import { RpcServer } from "effect/rpc";
|
|
5
|
-
/** What a protocol does with the events of the sockets carrying its tag. */
|
|
6
5
|
interface Receiver {
|
|
7
|
-
/** Hands a message to the protocol. */
|
|
8
6
|
readonly message: (socket: WebSocket, data: string | ArrayBuffer) => Effect.Effect<void>;
|
|
9
|
-
/** Tells the protocol the socket closed or failed. */
|
|
10
7
|
readonly close: (socket: WebSocket) => Effect.Effect<void>;
|
|
11
8
|
}
|
|
12
9
|
declare const Sockets_base: Context.ServiceClass<Sockets, "effect-actions-cloudflare/ActionDurableObject/Sockets", {
|
|
@@ -41,7 +38,6 @@ declare const Sockets_base: Context.ServiceClass<Sockets, "effect-actions-cloudf
|
|
|
41
38
|
* - A socket accepted by an earlier version of the routes at another `path` carries a tag no protocol holds now, and is answered `false`: close it, or serve that `path` again.
|
|
42
39
|
*/
|
|
43
40
|
export declare class Sockets extends Sockets_base {
|
|
44
|
-
/** One dispatcher, with no protocol registered, for the instance whose `State` it is given. */
|
|
45
41
|
static readonly layer: Layer.Layer<Sockets, never, State>;
|
|
46
42
|
}
|
|
47
43
|
/**
|
package/dist/sockets.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { HttpRouter, HttpServerRequest, HttpServerResponse } from "effect/http";
|
|
|
6
6
|
import { RpcMessage, RpcSerialization, RpcServer } from "effect/rpc";
|
|
7
7
|
import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
|
|
8
8
|
// src/durable/sockets.ts
|
|
9
|
+
const restartCloseCode = 1012;
|
|
9
10
|
const replyToClose = (socket, code, reason) => Effect.sync(() => {
|
|
10
11
|
if (code >= 1e3 && code !== 1005 && code !== 1006 && code !== 1015) closeQuietly(socket, code, reason);
|
|
11
12
|
else closeQuietly(socket);
|
|
@@ -23,7 +24,6 @@ const replyToClose = (socket, code, reason) => Effect.sync(() => {
|
|
|
23
24
|
* - A socket accepted by an earlier version of the routes at another `path` carries a tag no protocol holds now, and is answered `false`: close it, or serve that `path` again.
|
|
24
25
|
*/
|
|
25
26
|
var Sockets = class Sockets extends Context.Service()("effect-actions-cloudflare/ActionDurableObject/Sockets") {
|
|
26
|
-
/** One dispatcher, with no protocol registered, for the instance whose `State` it is given. */
|
|
27
27
|
static layer = Layer.effect(Sockets)(Effect.gen(function* () {
|
|
28
28
|
const state = yield* State;
|
|
29
29
|
const receivers = /* @__PURE__ */ new Map();
|
|
@@ -54,15 +54,13 @@ const Attachment = Schema.Struct({
|
|
|
54
54
|
pending: Schema.optional(Schema.Array(RequestId))
|
|
55
55
|
});
|
|
56
56
|
const decodeAttachment = Schema.decodeUnknownExit(Schema.NullishOr(Attachment));
|
|
57
|
-
/** The socket's attachment, failing when it does not decode, or `undefined` with none. */
|
|
58
57
|
const attachmentOf = (socket) => Exit.map(decodeAttachment(socket.deserializeAttachment()), (held) => held ?? void 0);
|
|
59
|
-
/** The socket's attachment, or `undefined` with none or one that does not decode. */
|
|
60
58
|
const heldOf = (socket) => Exit.match(attachmentOf(socket), {
|
|
61
59
|
onFailure: Function.constUndefined,
|
|
62
60
|
onSuccess: (held) => held
|
|
63
61
|
});
|
|
64
62
|
const pendingOf = (socket) => heldOf(socket)?.pending ?? [];
|
|
65
|
-
const
|
|
63
|
+
const heldInAttachment = (socket, update) => {
|
|
66
64
|
const held = heldOf(socket);
|
|
67
65
|
if (held === void 0) return true;
|
|
68
66
|
const pending = update(held.pending ?? []);
|
|
@@ -87,7 +85,7 @@ const upgraded = (client) => HttpServerResponse.raw(new Response(null, {
|
|
|
87
85
|
const closeReasonBytes = 123;
|
|
88
86
|
const bytesOf = (text) => new TextEncoder().encode(text).byteLength;
|
|
89
87
|
const acceptedCode = (code) => Number.isInteger(code) && code >= 4e3 && code <= 4999;
|
|
90
|
-
const
|
|
88
|
+
const validatedRefusal = (protocol) => Effect.gen(function* () {
|
|
91
89
|
if (protocol === void 0) return;
|
|
92
90
|
const { version, code, reason = `This server speaks protocol ${version}: reload the page` } = protocol;
|
|
93
91
|
if (!acceptedCode(code)) return yield* Effect.die(/* @__PURE__ */ new Error(`ActionDurableObject.layerWebSocket's protocol.code ${code} is not one it accepts, an integer from 4000 to 4999, the codes a watch ends at: give one of those`));
|
|
@@ -119,16 +117,11 @@ const refuseUpgrade = (code, reason) => Effect.suspend(() => {
|
|
|
119
117
|
server.close(code, reason);
|
|
120
118
|
return Effect.succeed(upgraded(client));
|
|
121
119
|
});
|
|
122
|
-
/**
|
|
123
|
-
* A hibernating `RpcServer.Protocol` over the object's WebSockets, speaking JSON, and its
|
|
124
|
-
* upgrade route at `path`. Each socket carries two tags, its group's and its own id's, and keeps
|
|
125
|
-
* its upgrade request's headers in its attachment, so both survive hibernation.
|
|
126
|
-
*/
|
|
127
120
|
const makeProtocol = (state, sockets, { path, protocol: version }) => Effect.gen(function* () {
|
|
128
121
|
const router = yield* HttpRouter.HttpRouter;
|
|
129
122
|
const context = yield* Effect.context();
|
|
130
123
|
const group = groupTag(path);
|
|
131
|
-
const refusal = yield*
|
|
124
|
+
const refusal = yield* validatedRefusal(version);
|
|
132
125
|
const idOf = (socket) => socketIdOf(state.getTags(socket));
|
|
133
126
|
state.setWebSocketAutoResponse(new WebSocketRequestResponsePair(String(parser.encode(RpcMessage.constPing)), String(parser.encode(RpcMessage.constPong))));
|
|
134
127
|
const disconnects = yield* Queue.make();
|
|
@@ -136,7 +129,7 @@ const makeProtocol = (state, sockets, { path, protocol: version }) => Effect.gen
|
|
|
136
129
|
const send = (clientId, response) => sendFrame(state, clientId, response).pipe(Effect.andThen(Effect.sync(() => {
|
|
137
130
|
const settled = settledBy(response);
|
|
138
131
|
if (settled === void 0) return;
|
|
139
|
-
for (const socket of state.getWebSockets(socketTag(clientId)))
|
|
132
|
+
for (const socket of state.getWebSockets(socketTag(clientId))) heldInAttachment(socket, settled);
|
|
140
133
|
})), Effect.andThen(subscriptions.sent(clientId, response)));
|
|
141
134
|
const protocol = yield* RpcServer.Protocol.make((write) => {
|
|
142
135
|
if (!sockets.register(group, {
|
|
@@ -146,7 +139,7 @@ const makeProtocol = (state, sockets, { path, protocol: version }) => Effect.gen
|
|
|
146
139
|
const headers = (yield* Effect.orDie(attachmentOf(socket)))?.headers ?? [];
|
|
147
140
|
const messages = parser.decode(Predicate.isString(data) ? data : new Uint8Array(data));
|
|
148
141
|
const sent = messages.flatMap((message) => Predicate.isTagged(message, "Request") ? [message.id] : []);
|
|
149
|
-
const kept = sent.length === 0 ||
|
|
142
|
+
const kept = sent.length === 0 || heldInAttachment(socket, (pending) => [...pending, ...sent]);
|
|
150
143
|
return yield* Effect.forEach(messages, (message) => {
|
|
151
144
|
if (!Predicate.isTagged(message, "Request")) return write(clientId, message);
|
|
152
145
|
return kept ? write(clientId, {
|
|
@@ -166,7 +159,7 @@ const makeProtocol = (state, sockets, { path, protocol: version }) => Effect.gen
|
|
|
166
159
|
})) return Effect.die(/* @__PURE__ */ new Error(`Two ActionDurableObject.layerWebSocket protocols of one object serve the path ${path}: a router prefix does not tell them apart: give each its own path, such as /a/rpc and /b/rpc`));
|
|
167
160
|
const resumed = subscriptions.resumed(write, (socket, kept) => {
|
|
168
161
|
if (pendingOf(socket).every(kept)) return true;
|
|
169
|
-
closeQuietly(socket,
|
|
162
|
+
closeQuietly(socket, restartCloseCode, lostCalls);
|
|
170
163
|
return false;
|
|
171
164
|
});
|
|
172
165
|
return Effect.as(resumed, {
|
package/dist/stub.d.mts
CHANGED
|
@@ -12,15 +12,15 @@ export interface Stub {
|
|
|
12
12
|
}
|
|
13
13
|
/**
|
|
14
14
|
* The `RpcClient.Protocol` `RpcClient.make` and `ActionRpc.client` call an object's rpcs with,
|
|
15
|
-
* over
|
|
15
|
+
* over the stub `stubOf` returns as each call is sent: each call is one call
|
|
16
16
|
* of the object's `effectActionsRpc` method, its messages and their answer passed as values,
|
|
17
17
|
* each schema's plain form, which the object serves through `make`'s or `handlers`' `calls`. The stub's own failure is an `RpcClientError`, as an HTTP transport's
|
|
18
18
|
* is, in every call's errors.
|
|
19
19
|
*
|
|
20
20
|
* **Gotchas**
|
|
21
21
|
*
|
|
22
|
-
* -
|
|
23
|
-
* - An `RpcClientError`'s `RpcClientDefect` holds the runtime's error as its `cause`: `.retryable` true allows repeating the call, as Cloudflare's guide says, and `.durableObjectReset` true needs a new stub, which
|
|
22
|
+
* - `stubOf` is called as each call is sent, `() => env.ROOM.getByName(name)`, for a new stub each time, since many exceptions leave a stub broken, failing every later call through it with the same error; one that throws is a defect, not an `RpcClientError`, as it is no failure of the stub.
|
|
23
|
+
* - An `RpcClientError`'s `RpcClientDefect` holds the runtime's error as its `cause`: `.retryable` true allows repeating the call, as Cloudflare's guide says, and `.durableObjectReset` true needs a new stub, which the next call's `stubOf` gives.
|
|
24
24
|
* - Workers RPC limits a call's arguments and its answer to 32 MiB, serialized: past it, the call fails with the runtime's `limited to 32MiB` error.
|
|
25
25
|
* - An object serving no `calls`, once built, or a class without the method, fails each call, with the object's or the runtime's message as the error's cause; an object whose build fails fails it with that failure first.
|
|
26
26
|
* - A call's interruption does not reach the object, as a stub call cannot be cancelled: its answer is dropped.
|
|
@@ -29,7 +29,7 @@ export interface Stub {
|
|
|
29
29
|
*
|
|
30
30
|
* Guide: `docs/ActionDurableObject-stub.md` in this package.
|
|
31
31
|
*/
|
|
32
|
-
export declare const layerStubClient: (
|
|
32
|
+
export declare const layerStubClient: (stubOf: () => Stub) => Layer.Layer<RpcClient.Protocol>;
|
|
33
33
|
type Unary<Rpcs extends Rpc.Any> = Rpcs extends Rpc.Any ? [Rpc.SuccessSchema<Rpcs>] extends [RpcSchema.Stream<infer _A, infer _E>] ? never : Rpcs : never;
|
|
34
34
|
/**
|
|
35
35
|
* A group's rpcs, as `client.<rpc>(payload)`: Effect's native client, each call failing with
|
package/dist/stub.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { RpcClient, RpcClientError, RpcSchema } from "effect/rpc";
|
|
|
6
6
|
const clientError = (reason) => new RpcClientError.RpcClientError({ reason });
|
|
7
7
|
const isAnswer = Schema.is(Schema.Array(Schema.Struct({ _tag: Schema.String })));
|
|
8
8
|
const completionOf = Schema.decodeUnknownOption(Schema.Union([Schema.TaggedStruct("Exit", { requestId: RequestId }), Schema.Struct({ _tag: Schema.Literals(["Defect", "ClientProtocolError"]) })]));
|
|
9
|
+
const completesCall = (answer, request) => answer.some((response) => Option.exists(completionOf(response), (completion) => !Predicate.isTagged(completion, "Exit") || completion.requestId === request.id));
|
|
9
10
|
const makeProtocol = (target) => RpcClient.Protocol.make((write) => Effect.succeed({
|
|
10
11
|
send: (clientId, request) => Predicate.isTagged(request, "Request") ? Effect.gen(function* () {
|
|
11
12
|
const stub = yield* target;
|
|
@@ -16,7 +17,7 @@ const makeProtocol = (target) => RpcClient.Protocol.make((write) => Effect.succe
|
|
|
16
17
|
cause
|
|
17
18
|
}))
|
|
18
19
|
});
|
|
19
|
-
if (!(isAnswer(answer) && answer
|
|
20
|
+
if (!(isAnswer(answer) && completesCall(answer, request))) return yield* clientError(new RpcClientError.RpcClientDefect({
|
|
20
21
|
message: "The object's answer ended before the call completed: delegate effectActionsRpc to handlers' own, returning its answer",
|
|
21
22
|
cause: request
|
|
22
23
|
}));
|
|
@@ -28,15 +29,15 @@ const makeProtocol = (target) => RpcClient.Protocol.make((write) => Effect.succe
|
|
|
28
29
|
}));
|
|
29
30
|
/**
|
|
30
31
|
* The `RpcClient.Protocol` `RpcClient.make` and `ActionRpc.client` call an object's rpcs with,
|
|
31
|
-
* over
|
|
32
|
+
* over the stub `stubOf` returns as each call is sent: each call is one call
|
|
32
33
|
* of the object's `effectActionsRpc` method, its messages and their answer passed as values,
|
|
33
34
|
* each schema's plain form, which the object serves through `make`'s or `handlers`' `calls`. The stub's own failure is an `RpcClientError`, as an HTTP transport's
|
|
34
35
|
* is, in every call's errors.
|
|
35
36
|
*
|
|
36
37
|
* **Gotchas**
|
|
37
38
|
*
|
|
38
|
-
* -
|
|
39
|
-
* - An `RpcClientError`'s `RpcClientDefect` holds the runtime's error as its `cause`: `.retryable` true allows repeating the call, as Cloudflare's guide says, and `.durableObjectReset` true needs a new stub, which
|
|
39
|
+
* - `stubOf` is called as each call is sent, `() => env.ROOM.getByName(name)`, for a new stub each time, since many exceptions leave a stub broken, failing every later call through it with the same error; one that throws is a defect, not an `RpcClientError`, as it is no failure of the stub.
|
|
40
|
+
* - An `RpcClientError`'s `RpcClientDefect` holds the runtime's error as its `cause`: `.retryable` true allows repeating the call, as Cloudflare's guide says, and `.durableObjectReset` true needs a new stub, which the next call's `stubOf` gives.
|
|
40
41
|
* - Workers RPC limits a call's arguments and its answer to 32 MiB, serialized: past it, the call fails with the runtime's `limited to 32MiB` error.
|
|
41
42
|
* - An object serving no `calls`, once built, or a class without the method, fails each call, with the object's or the runtime's message as the error's cause; an object whose build fails fails it with that failure first.
|
|
42
43
|
* - A call's interruption does not reach the object, as a stub call cannot be cancelled: its answer is dropped.
|
|
@@ -45,9 +46,9 @@ const makeProtocol = (target) => RpcClient.Protocol.make((write) => Effect.succe
|
|
|
45
46
|
*
|
|
46
47
|
* Guide: `docs/ActionDurableObject-stub.md` in this package.
|
|
47
48
|
*/
|
|
48
|
-
const layerStubClient = (
|
|
49
|
-
const
|
|
50
|
-
const currentStub = Effect.flatMap(Effect.service(
|
|
49
|
+
const layerStubClient = (stubOf) => Layer.effect(RpcClient.Protocol)(makeProtocol(Effect.sync(stubOf)));
|
|
50
|
+
const StubOfCurrentCall = Context.Reference("effect-actions-cloudflare/ActionDurableObject/CurrentStub", { defaultValue: Function.constUndefined });
|
|
51
|
+
const currentStub = Effect.flatMap(Effect.service(StubOfCurrentCall), (stubOf) => stubOf === void 0 ? Effect.die(/* @__PURE__ */ new Error("No stub for the call")) : Effect.sync(stubOf));
|
|
51
52
|
/**
|
|
52
53
|
* The rpcs of `group`, as `(stubOf) => client`, for a Worker or an object calling objects of
|
|
53
54
|
* one class: `room(() => env.CHAT_ROOM.getByName(name)).latestAt()`. One native client, built
|
|
@@ -70,6 +71,6 @@ const client = (group) => Effect.gen(function* () {
|
|
|
70
71
|
const native = yield* RpcClient.make(group).pipe(Effect.provideServiceEffect(RpcClient.Protocol, makeProtocol(currentStub)), Effect.map((made) => made));
|
|
71
72
|
const rpcs = group.requests.values();
|
|
72
73
|
const unary = new Set([...rpcs].flatMap((rpc) => RpcSchema.isStreamSchema(rpc.successSchema) ? [] : [rpc._tag]));
|
|
73
|
-
return (stubOf) => Object.fromEntries(Object.entries(native).flatMap(([tag, method]) => unary.has(tag) ? [[tag, (...args) => method(...args).pipe(Effect.provideService(
|
|
74
|
+
return (stubOf) => Object.fromEntries(Object.entries(native).flatMap(([tag, method]) => unary.has(tag) ? [[tag, (...args) => method(...args).pipe(Effect.provideService(StubOfCurrentCall, stubOf))]] : []));
|
|
74
75
|
});
|
|
75
76
|
export { client, layerStubClient };
|
package/dist/subscriptions.mjs
CHANGED
|
@@ -2,7 +2,6 @@ import { deleted } from "./batches.mjs";
|
|
|
2
2
|
import { RequestId, closeQuietly, groupTag, socketIdOf } from "./frames.mjs";
|
|
3
3
|
import { Effect, Function, Match, Option, Predicate, Schema } from "effect";
|
|
4
4
|
// src/durable/subscriptions.ts
|
|
5
|
-
/** Where an object keeps its sockets' event subscriptions. */
|
|
6
5
|
const keptPrefix = "effect-actions/subscription/";
|
|
7
6
|
const Request = Schema.TaggedStruct("Request", {
|
|
8
7
|
id: RequestId,
|
|
@@ -11,33 +10,27 @@ const Request = Schema.TaggedStruct("Request", {
|
|
|
11
10
|
});
|
|
12
11
|
const requestOf = Schema.decodeUnknownOption(Request);
|
|
13
12
|
const payloadOf = Schema.decodeUnknownOption(Schema.Json);
|
|
13
|
+
const resumeFailedCode = 1e3;
|
|
14
14
|
const unresumable = "This server could not resume this socket's subscriptions: connect again";
|
|
15
15
|
const suffixOf = (socket, id) => `${socket}#${Predicate.isNumber(id) ? "n" : "s"}${id}`;
|
|
16
16
|
const socketOf = (suffix) => Number(suffix.slice(0, suffix.indexOf("#")));
|
|
17
|
-
/**
|
|
18
|
-
* The event subscriptions of the sockets one `layerWebSocket` path accepted, kept in storage:
|
|
19
|
-
* `keeper` stores each request as `ActionRpc.layer` admits it, `sent` stores the payload that
|
|
20
|
-
* resumes it with each chunk sent and forgets it at its exit, `closed` forgets a socket's, and
|
|
21
|
-
* `resumed` writes each kept one whose socket of this path is still open to the server again,
|
|
22
|
-
* from there.
|
|
23
|
-
*/
|
|
24
17
|
const makeSubscriptions = (state, path) => {
|
|
25
18
|
const { storage } = state;
|
|
26
19
|
const group = groupTag(path);
|
|
27
20
|
const requests = `${keptPrefix}request/${path}#`;
|
|
28
21
|
const payloads = `${keptPrefix}payload/${path}#`;
|
|
29
22
|
const keysOf = (suffix) => [`${requests}${suffix}`, `${payloads}${suffix}`];
|
|
30
|
-
const
|
|
23
|
+
const liveSubscriptionsMirror = /* @__PURE__ */ new Map();
|
|
31
24
|
const listed = (prefix) => Effect.map(Effect.promise(async () => await storage.list({ prefix })), (found) => [...found].map(([key, value]) => [key.slice(prefix.length), value]));
|
|
32
25
|
const closed = (socket) => Effect.suspend(() => {
|
|
33
|
-
const owned = [...
|
|
34
|
-
for (const suffix of owned)
|
|
26
|
+
const owned = [...liveSubscriptionsMirror.keys()].filter((suffix) => socketOf(suffix) === socket);
|
|
27
|
+
for (const suffix of owned) liveSubscriptionsMirror.delete(suffix);
|
|
35
28
|
return deleted(storage, owned.flatMap(keysOf));
|
|
36
29
|
});
|
|
37
30
|
const keeper = { keep: ({ clientId, requestId, tag, payload, headers, resumeFrom }) => Effect.suspend(() => {
|
|
38
31
|
const suffix = suffixOf(clientId, requestId);
|
|
39
|
-
const resuming =
|
|
40
|
-
|
|
32
|
+
const resuming = liveSubscriptionsMirror.has(suffix);
|
|
33
|
+
liveSubscriptionsMirror.set(suffix, resumeFrom);
|
|
41
34
|
if (resuming) return Effect.void;
|
|
42
35
|
return Effect.promise(async () => {
|
|
43
36
|
await storage.put({
|
|
@@ -51,23 +44,18 @@ const makeSubscriptions = (state, path) => {
|
|
|
51
44
|
});
|
|
52
45
|
}) };
|
|
53
46
|
const moved = (suffix, values) => Effect.suspend(() => {
|
|
54
|
-
const payload =
|
|
47
|
+
const payload = liveSubscriptionsMirror.get(suffix)?.(values);
|
|
55
48
|
return payload === void 0 ? Effect.void : Effect.promise(async () => {
|
|
56
49
|
await storage.put(`${payloads}${suffix}`, payload);
|
|
57
50
|
});
|
|
58
51
|
});
|
|
59
52
|
const sent = (socket, response) => Match.value(response).pipe(Match.tag("Defect", () => closed(socket)), Match.tag("Chunk", ({ requestId, values }) => moved(suffixOf(socket, requestId), values)), Match.tag("Exit", ({ requestId }) => Effect.suspend(() => {
|
|
60
53
|
const suffix = suffixOf(socket, requestId);
|
|
61
|
-
return
|
|
54
|
+
return liveSubscriptionsMirror.delete(suffix) ? Effect.promise(async () => await storage.delete(keysOf(suffix))) : Effect.void;
|
|
62
55
|
})), Match.orElse(() => Effect.void));
|
|
63
56
|
const unresumed = (socket) => Effect.andThen(Effect.logWarning(`layerWebSocket at ${path} could not resume a socket's kept event subscriptions, and closes it`), Effect.sync(() => {
|
|
64
|
-
closeQuietly(socket,
|
|
57
|
+
closeQuietly(socket, resumeFailedCode, unresumable);
|
|
65
58
|
}));
|
|
66
|
-
/**
|
|
67
|
-
* Writes each kept subscription of a socket of this path still open to the server, from where
|
|
68
|
-
* it reached, and forgets the rest: `held` answers whether a socket stays open, given whether
|
|
69
|
-
* one of its requests is a subscription kept for it, and closes one that does not.
|
|
70
|
-
*/
|
|
71
59
|
const resumed = (write, held) => Effect.gen(function* () {
|
|
72
60
|
const kept = yield* listed(requests);
|
|
73
61
|
const resumeFrom = new Map(yield* listed(payloads));
|
|
@@ -88,7 +76,7 @@ const makeSubscriptions = (state, path) => {
|
|
|
88
76
|
const unread = new Set(entries.flatMap(({ socket, request }) => open.has(socket) && Option.isNone(request) ? [socket] : []));
|
|
89
77
|
const gone = [...resumeFrom.keys()].flatMap((suffix) => keptIds.has(suffix) ? [] : [`${payloads}${suffix}`]);
|
|
90
78
|
for (const { suffix, socket, request } of entries) if (open.has(socket) && !unread.has(socket) && Option.isSome(request)) {
|
|
91
|
-
|
|
79
|
+
liveSubscriptionsMirror.set(suffix, Function.constUndefined);
|
|
92
80
|
yield* write(socket, request.value);
|
|
93
81
|
} else gone.push(...keysOf(suffix));
|
|
94
82
|
yield* deleted(storage, gone);
|
package/dist/tasks.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { Clock, Context, Effect } from "effect";
|
|
2
2
|
// src/durable/tasks.ts
|
|
3
|
-
/** The alarm owner a task's time is kept under, one per task name. */
|
|
4
3
|
const taskOwner = (name) => `task:${name}`;
|
|
5
4
|
/**
|
|
6
5
|
* Runs the object's tasks, `make`'s or `handlers`' `tasks`, by name, after a change, on the
|
|
@@ -15,16 +14,12 @@ const taskOwner = (name) => `task:${name}`;
|
|
|
15
14
|
* - A run that fails is logged and retried as the alarm retries an owner: 2 seconds later, doubling, six times, then hourly.
|
|
16
15
|
*/
|
|
17
16
|
var Tasks = class extends Context.Service()("effect-actions-cloudflare/ActionDurableObject/Tasks") {};
|
|
18
|
-
/**
|
|
19
|
-
* The instance's `Tasks` over its `schedule`, and the alarm owners that run each of `tasks`, by
|
|
20
|
-
* the name `runAfter` takes.
|
|
21
|
-
*/
|
|
22
17
|
const makeTasks = (schedule, tasks) => ({
|
|
23
18
|
tasks: Tasks.of({ runAfter: (name, change) => Effect.gen(function* () {
|
|
24
19
|
if (!Object.hasOwn(tasks, name)) return yield* Effect.die(/* @__PURE__ */ new Error(`ActionDurableObject.Tasks: this object has no task ${name} (give make or handlers a tasks entry named ${name})`));
|
|
25
|
-
const
|
|
26
|
-
yield*
|
|
27
|
-
return yield* Effect.ensuring(change,
|
|
20
|
+
const markDueNow = schedule.set(taskOwner(name), yield* Clock.currentTimeMillis);
|
|
21
|
+
yield* markDueNow;
|
|
22
|
+
return yield* Effect.ensuring(change, markDueNow);
|
|
28
23
|
}) }),
|
|
29
24
|
owners: Object.fromEntries(Object.entries(tasks).map(([name, task]) => [taskOwner(name), () => task]))
|
|
30
25
|
});
|