@gjermundgaraba/effect-actions 0.20.0 → 0.21.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/README.md +19 -41
- package/dist/Action.d.mts +140 -46
- package/dist/Action.mjs +87 -42
- package/dist/ActionCli.d.mts +69 -33
- package/dist/ActionCli.mjs +51 -32
- package/dist/ActionHttp.d.mts +69 -35
- package/dist/ActionHttp.mjs +75 -23
- package/dist/ActionMcp.d.mts +56 -22
- package/dist/ActionMcp.mjs +94 -69
- package/dist/ActionRpc.d.mts +222 -55
- package/dist/ActionRpc.mjs +299 -46
- package/dist/ActionToolkit.d.mts +22 -5
- package/dist/ActionToolkit.mjs +36 -12
- package/dist/Authentication.d.mts +42 -10
- package/dist/Authentication.mjs +37 -30
- package/dist/Event.d.mts +370 -0
- package/dist/Event.mjs +103 -0
- package/dist/Testing.mjs +1 -1
- package/dist/Webhook.d.mts +146 -0
- package/dist/Webhook.mjs +72 -0
- package/dist/authentication/provider.d.mts +35 -10
- package/dist/authentication/provider.mjs +1 -1
- package/dist/authentication/refusal.mjs +2 -2
- package/dist/cli/commands.d.mts +9 -5
- package/dist/cli/commands.mjs +84 -33
- package/dist/contract/ast.mjs +4 -1
- package/dist/contract/authorizer.d.mts +8 -0
- package/dist/contract/authorizer.mjs +20 -0
- package/dist/contract/call.d.mts +9 -6
- package/dist/contract/call.mjs +48 -6
- package/dist/contract/callback.d.mts +7 -0
- package/dist/contract/callback.mjs +27 -0
- package/dist/contract/caller.d.mts +13 -2
- package/dist/contract/caller.mjs +22 -3
- package/dist/contract/canonical.mjs +10 -0
- package/dist/contract/codec.d.mts +9 -0
- package/dist/contract/codec.mjs +9 -0
- package/dist/contract/codes.mjs +81 -0
- package/dist/contract/declared.mjs +25 -9
- package/dist/contract/digest.mjs +6 -0
- package/dist/contract/errors.d.mts +9 -3
- package/dist/contract/errors.mjs +1 -1
- package/dist/contract/feed.d.mts +127 -0
- package/dist/contract/feed.mjs +249 -0
- package/dist/contract/hub.d.mts +94 -0
- package/dist/contract/hub.mjs +133 -0
- package/dist/contract/idempotency.d.mts +47 -0
- package/dist/contract/idempotency.mjs +99 -0
- package/dist/contract/implementation.d.mts +73 -47
- package/dist/contract/implementation.mjs +92 -58
- package/dist/contract/input-check.d.mts +15 -0
- package/dist/contract/input-check.mjs +16 -0
- package/dist/contract/lease.mjs +28 -0
- package/dist/contract/listed.d.mts +4 -0
- package/dist/contract/listed.mjs +22 -0
- package/dist/contract/log.d.mts +41 -0
- package/dist/contract/log.mjs +144 -0
- package/dist/contract/names.mjs +14 -4
- package/dist/contract/occurrence.d.mts +29 -0
- package/dist/contract/occurrence.mjs +45 -0
- package/dist/contract/pages.d.mts +13 -0
- package/dist/contract/pages.mjs +39 -0
- package/dist/contract/progress.d.mts +26 -0
- package/dist/contract/progress.mjs +72 -0
- package/dist/contract/push.d.mts +20 -0
- package/dist/contract/push.mjs +96 -0
- package/dist/contract/receiving.d.mts +29 -0
- package/dist/contract/receiving.mjs +42 -0
- package/dist/contract/recent.mjs +12 -0
- package/dist/contract/reported.mjs +16 -0
- package/dist/contract/serving.mjs +63 -0
- package/dist/contract/status.mjs +14 -5
- package/dist/contract/stream.d.mts +34 -0
- package/dist/contract/stream.mjs +31 -0
- package/dist/contract/subscribe.d.mts +45 -0
- package/dist/contract/subscribe.mjs +50 -0
- package/dist/contract/transient.mjs +28 -0
- package/dist/contract/watch.d.mts +18 -0
- package/dist/contract/watch.mjs +105 -0
- package/dist/contract/webhooks.d.mts +42 -0
- package/dist/contract/webhooks.mjs +53 -0
- package/dist/http/client.d.mts +37 -12
- package/dist/http/client.mjs +51 -12
- package/dist/http/events.d.mts +56 -0
- package/dist/http/events.mjs +102 -0
- package/dist/http/placed.d.mts +36 -0
- package/dist/http/placed.mjs +71 -0
- package/dist/http/schema-errors.mjs +1 -1
- package/dist/http/streamed.d.mts +21 -0
- package/dist/http/streamed.mjs +94 -0
- package/dist/http/watch.mjs +50 -0
- package/dist/mcp/bridge.mjs +283 -0
- package/dist/mcp/client.d.mts +30 -97
- package/dist/mcp/client.mjs +59 -132
- package/dist/mcp/events.mjs +142 -0
- package/dist/mcp/progress.mjs +34 -0
- package/dist/mcp/protocol.mjs +39 -13
- package/dist/mcp/reply.d.mts +79 -0
- package/dist/mcp/reply.mjs +99 -0
- package/dist/mcp/request.d.mts +40 -0
- package/dist/mcp/request.mjs +51 -0
- package/dist/mcp/watch.mjs +115 -0
- package/dist/rpc/watch.mjs +39 -0
- package/dist/stdio/console.mjs +13 -5
- package/dist/testing/memory.d.mts +1 -1
- package/dist/testing/memory.mjs +3 -3
- package/dist/webhook/address.mjs +47 -0
- package/dist/webhook/attempt.mjs +65 -0
- package/dist/webhook/receiver.mjs +67 -0
- package/dist/webhook/signing.mjs +28 -0
- package/dist/webhook/stored.mjs +57 -0
- package/dist/webhook/subscriptions.mjs +342 -0
- package/docs/Action-failures.md +2 -2
- package/docs/Action-streams.md +115 -0
- package/docs/Action.md +58 -124
- package/docs/ActionCli-failures.md +1 -2
- package/docs/ActionCli.md +83 -88
- package/docs/ActionHttp-failures.md +3 -3
- package/docs/ActionHttp.md +90 -70
- package/docs/ActionMcp-failures.md +2 -1
- package/docs/ActionMcp.md +120 -92
- package/docs/ActionRpc-events.md +126 -0
- package/docs/ActionRpc-failures.md +1 -2
- package/docs/ActionRpc.md +108 -79
- package/docs/ActionToolkit-failures.md +1 -2
- package/docs/ActionToolkit.md +33 -90
- package/docs/Authentication-advanced.md +52 -60
- package/docs/Authentication-failures.md +5 -4
- package/docs/Authentication.md +23 -58
- package/docs/Event-failures.md +15 -0
- package/docs/Event.md +281 -0
- package/docs/README.md +51 -82
- package/docs/Testing-failures.md +1 -5
- package/docs/Testing.md +126 -86
- package/docs/Webhook-failures.md +8 -0
- package/docs/Webhook.md +319 -0
- package/docs/setup-failures.md +2 -3
- package/docs/setup.md +32 -52
- package/examples/README.md +12 -37
- package/examples/assistant/README.md +21 -26
- package/examples/assistant/src/agent.ts +66 -24
- package/examples/assistant/src/cli.ts +7 -4
- package/examples/assistant/src/commands.ts +9 -4
- package/examples/assistant/src/identity/Identity.ts +5 -4
- package/examples/assistant/src/identity/authorize.ts +13 -5
- package/examples/assistant/src/identity/handlers.ts +3 -2
- package/examples/assistant/src/identity/permissions.ts +9 -3
- package/examples/assistant/src/platform/tasks-file.ts +15 -11
- package/examples/assistant/src/scripted.ts +7 -4
- package/examples/assistant/src/tasks/Tasks.ts +20 -6
- package/examples/assistant/src/tasks/handlers.ts +9 -5
- package/examples/assistant/src/tasks/store.ts +38 -11
- package/examples/assistant/src/toolkit.ts +11 -7
- package/examples/assistant/test/commands.test.ts +53 -27
- package/examples/assistant/test/identity/identity.test.ts +27 -11
- package/examples/assistant/test/platform/tasks-file.test.ts +39 -23
- package/examples/assistant/test/tasks/tasks.test.ts +62 -34
- package/examples/assistant/test/toolkit.test.ts +104 -75
- package/examples/build-events/README.md +83 -0
- package/examples/build-events/src/Api.ts +27 -0
- package/examples/build-events/src/Rpc.ts +26 -0
- package/examples/build-events/src/builds/Builds.ts +108 -0
- package/examples/build-events/src/builds/events.ts +54 -0
- package/examples/build-events/src/builds/handlers.ts +58 -0
- package/examples/build-events/src/builds/log.ts +30 -0
- package/examples/build-events/src/builds/status.ts +28 -0
- package/examples/build-events/src/builds/store.ts +130 -0
- package/examples/build-events/src/cli.ts +18 -0
- package/examples/build-events/src/commands.ts +32 -0
- package/examples/build-events/src/deploys/Deploys.ts +44 -0
- package/examples/build-events/src/deploys/approvers.ts +55 -0
- package/examples/build-events/src/deploys/events.ts +32 -0
- package/examples/build-events/src/deploys/handlers.ts +38 -0
- package/examples/build-events/src/identity/Identity.ts +29 -0
- package/examples/build-events/src/identity/members.ts +7 -0
- package/examples/build-events/src/identity/verifier.ts +22 -0
- package/examples/build-events/src/notifier/Notices.ts +26 -0
- package/examples/build-events/src/notifier/routes.ts +71 -0
- package/examples/build-events/src/notifier.ts +48 -0
- package/examples/build-events/src/routes.ts +71 -0
- package/examples/build-events/src/server.ts +62 -0
- package/examples/build-events/test/builds/events.test.ts +68 -0
- package/examples/build-events/test/builds/log.test.ts +71 -0
- package/examples/build-events/test/builds/progress.test.ts +57 -0
- package/examples/build-events/test/builds/status.test.ts +42 -0
- package/examples/build-events/test/commands.test.ts +103 -0
- package/examples/build-events/test/deploys/events.test.ts +52 -0
- package/examples/build-events/test/http.test.ts +95 -0
- package/examples/build-events/test/notifier.test.ts +80 -0
- package/examples/build-events/test/routes.test.ts +96 -0
- package/examples/build-events/test/rpc.test.ts +84 -0
- package/examples/build-events/test/support/as.ts +8 -0
- package/examples/build-events/test/support/memory.ts +43 -0
- package/examples/build-events/test/webhooks.test.ts +143 -0
- package/examples/kitchen-sink/README.md +178 -166
- package/examples/kitchen-sink/src/Actions.ts +11 -3
- package/examples/kitchen-sink/src/Api.ts +12 -6
- package/examples/kitchen-sink/src/Rpc.ts +7 -5
- package/examples/kitchen-sink/src/admin/Admin.ts +21 -10
- package/examples/kitchen-sink/src/admin/authenticate.ts +8 -4
- package/examples/kitchen-sink/src/admin/authorize.ts +13 -5
- package/examples/kitchen-sink/src/admin/grants.ts +5 -3
- package/examples/kitchen-sink/src/admin/handlers.ts +9 -5
- package/examples/kitchen-sink/src/agent.ts +14 -6
- package/examples/kitchen-sink/src/assistant/Assistant.ts +21 -15
- package/examples/kitchen-sink/src/assistant/conversations.ts +14 -9
- package/examples/kitchen-sink/src/assistant/demo-model.ts +2 -2
- package/examples/kitchen-sink/src/assistant/handlers.ts +42 -22
- package/examples/kitchen-sink/src/attachments/Attachments.ts +6 -6
- package/examples/kitchen-sink/src/attachments/download.ts +7 -6
- package/examples/kitchen-sink/src/attachments/handlers.ts +9 -4
- package/examples/kitchen-sink/src/attachments/store.ts +25 -18
- package/examples/kitchen-sink/src/browser.ts +10 -4
- package/examples/kitchen-sink/src/cli.ts +28 -12
- package/examples/kitchen-sink/src/config.ts +1 -1
- package/examples/kitchen-sink/src/identity/Identity.ts +24 -10
- package/examples/kitchen-sink/src/identity/authenticate.ts +19 -11
- package/examples/kitchen-sink/src/identity/authorize.ts +14 -4
- package/examples/kitchen-sink/src/identity/directory.ts +16 -7
- package/examples/kitchen-sink/src/identity/handlers.ts +1 -0
- package/examples/kitchen-sink/src/identity/members.ts +7 -2
- package/examples/kitchen-sink/src/identity/quota.ts +23 -20
- package/examples/kitchen-sink/src/issues/Issues.ts +91 -15
- package/examples/kitchen-sink/src/issues/assistant.ts +4 -2
- package/examples/kitchen-sink/src/issues/events.ts +46 -0
- package/examples/kitchen-sink/src/issues/export.ts +35 -0
- package/examples/kitchen-sink/src/issues/guide.ts +4 -3
- package/examples/kitchen-sink/src/issues/handlers.ts +20 -7
- package/examples/kitchen-sink/src/issues/store.ts +156 -67
- package/examples/kitchen-sink/src/issues/write-limit.ts +44 -18
- package/examples/kitchen-sink/src/memory.ts +11 -2
- package/examples/kitchen-sink/src/ops.ts +10 -5
- package/examples/kitchen-sink/src/request-policy.ts +21 -9
- package/examples/kitchen-sink/src/routes.ts +67 -27
- package/examples/kitchen-sink/src/sdk.ts +22 -12
- package/examples/kitchen-sink/src/seed.ts +15 -9
- package/examples/kitchen-sink/src/server.ts +24 -10
- package/examples/kitchen-sink/src/stdio.ts +9 -3
- package/examples/kitchen-sink/src/workspaces/Workspaces.ts +10 -7
- package/examples/kitchen-sink/src/workspaces/handlers.ts +3 -2
- package/examples/kitchen-sink/src/workspaces/registry.ts +16 -10
- package/examples/kitchen-sink/src/workspaces/tenant.ts +12 -5
- package/examples/kitchen-sink/test/admin/admin.test.ts +38 -23
- package/examples/kitchen-sink/test/assistant/assistant.test.ts +471 -299
- package/examples/kitchen-sink/test/attachments/attachments.test.ts +135 -94
- package/examples/kitchen-sink/test/identity/identity.test.ts +110 -82
- package/examples/kitchen-sink/test/identity/session.test.ts +65 -43
- package/examples/kitchen-sink/test/issues/assistant.test.ts +50 -31
- package/examples/kitchen-sink/test/issues/events.test.ts +273 -0
- package/examples/kitchen-sink/test/issues/export.test.ts +120 -0
- package/examples/kitchen-sink/test/issues/issues.test.ts +105 -75
- package/examples/kitchen-sink/test/request-policy.test.ts +108 -47
- package/examples/kitchen-sink/test/routes.test.ts +102 -57
- package/examples/kitchen-sink/test/support/as.ts +9 -0
- package/examples/kitchen-sink/test/support/scripted.ts +6 -5
- package/examples/kitchen-sink/test/support/services.ts +17 -0
- package/examples/live-rpc/README.md +33 -35
- package/examples/live-rpc/src/Rpc.ts +9 -5
- package/examples/live-rpc/src/browser.ts +39 -11
- package/examples/live-rpc/src/chat/Chat.ts +30 -16
- package/examples/live-rpc/src/chat/authorize.ts +2 -1
- package/examples/live-rpc/src/chat/events.ts +74 -0
- package/examples/live-rpc/src/chat/handlers.ts +13 -5
- package/examples/live-rpc/src/chat/room.ts +9 -5
- package/examples/live-rpc/src/chat/store.ts +51 -14
- package/examples/live-rpc/src/client.ts +22 -5
- package/examples/live-rpc/src/identity/Identity.ts +3 -3
- package/examples/live-rpc/src/identity/verifier.ts +8 -4
- package/examples/live-rpc/src/page.ts +58 -12
- package/examples/live-rpc/src/routes.ts +14 -10
- package/examples/live-rpc/src/server.ts +15 -5
- package/examples/live-rpc/test/browser.test.ts +127 -50
- package/examples/live-rpc/test/chat/chat.test.ts +215 -23
- package/examples/live-rpc/test/routes.test.ts +130 -46
- package/examples/live-rpc/test/server.test.ts +94 -33
- package/examples/mcp-tools/README.md +24 -38
- package/examples/mcp-tools/src/platform/fake-farm.ts +63 -24
- package/examples/mcp-tools/src/platform/print-farm.ts +59 -29
- package/examples/mcp-tools/src/printers/Printers.ts +18 -7
- package/examples/mcp-tools/src/printers/farm.ts +15 -5
- package/examples/mcp-tools/src/printers/handlers.ts +12 -6
- package/examples/mcp-tools/src/routes.ts +8 -6
- package/examples/mcp-tools/src/server.ts +12 -5
- package/examples/mcp-tools/src/stdio.ts +3 -2
- package/examples/mcp-tools/test/printers/printers.test.ts +132 -89
- package/examples/mcp-tools/test/routes.test.ts +140 -81
- package/examples/notes-service/README.md +30 -45
- package/examples/notes-service/src/Api.ts +1 -0
- package/examples/notes-service/src/browser.ts +15 -14
- package/examples/notes-service/src/cli.ts +8 -3
- package/examples/notes-service/src/commands.ts +46 -20
- package/examples/notes-service/src/identity/Identity.ts +2 -2
- package/examples/notes-service/src/identity/authorize.ts +6 -2
- package/examples/notes-service/src/identity/handlers.ts +3 -2
- package/examples/notes-service/src/identity/users.ts +2 -1
- package/examples/notes-service/src/identity/verifier.ts +12 -6
- package/examples/notes-service/src/notes/Notes.ts +46 -22
- package/examples/notes-service/src/notes/handlers.ts +18 -9
- package/examples/notes-service/src/notes/store.ts +70 -43
- package/examples/notes-service/src/routes.ts +7 -5
- package/examples/notes-service/src/server.ts +12 -5
- package/examples/notes-service/src/status/Status.ts +1 -1
- package/examples/notes-service/src/status/handlers.ts +5 -3
- package/examples/notes-service/src/stdio.ts +7 -3
- package/examples/notes-service/src/worker.ts +6 -3
- package/examples/notes-service/test/commands.test.ts +164 -91
- package/examples/notes-service/test/notes/as.ts +3 -1
- package/examples/notes-service/test/notes/idempotency.test.ts +97 -67
- package/examples/notes-service/test/notes/in-process.test.ts +26 -15
- package/examples/notes-service/test/notes/notes.test.ts +49 -31
- package/examples/notes-service/test/routes.test.ts +106 -71
- package/examples/notes-service/test/worker.test.ts +21 -13
- package/examples/quickstart/README.md +2 -6
- package/examples/quickstart/src/Api.ts +1 -0
- package/examples/quickstart/src/client.ts +6 -3
- package/examples/quickstart/src/greeting/Greeting.ts +1 -1
- package/examples/quickstart/src/greeting/handlers.ts +5 -2
- package/examples/quickstart/src/routes.ts +3 -2
- package/examples/quickstart/src/server.ts +6 -2
- package/examples/quickstart/test/routes.test.ts +3 -2
- package/package.json +10 -4
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import * as Event from "@gjermundgaraba/effect-actions/Event";
|
|
2
|
+
import { Effect, Option, Schema } from "effect";
|
|
3
|
+
|
|
4
|
+
import { authorize } from "./authorize.js";
|
|
5
|
+
import { MessagePosted } from "./Chat.js";
|
|
6
|
+
import { CurrentRoom } from "./room.js";
|
|
7
|
+
import { MessageStore } from "./store.js";
|
|
8
|
+
|
|
9
|
+
const idOf = Schema.decodeUnknownOption(
|
|
10
|
+
Schema.NumberFromString.pipe(Schema.check(Schema.isInt()))
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
// A cursor this source gives is `<epoch>:<id>`, the store's epoch and the id of the last message
|
|
14
|
+
// a page holds, so one from before a restart of the memory store is told apart. A page starting
|
|
15
|
+
// a watch gives the id alone, as `readMessages`' `after` is, which is the store's now.
|
|
16
|
+
const positionOf = (epoch: string, cursor: string | null) => {
|
|
17
|
+
const parts =
|
|
18
|
+
cursor?.includes(":") === true ? cursor.split(":") : [epoch, cursor];
|
|
19
|
+
|
|
20
|
+
return parts.length === 2 && parts[0] === epoch
|
|
21
|
+
? idOf(parts[1])
|
|
22
|
+
: Option.none();
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// The room's messages as an event, read from the store that keeps them: a check source.
|
|
26
|
+
// `postMessage` notifies after each post, so a page receives it at once, not at its next read
|
|
27
|
+
// every 5 seconds.
|
|
28
|
+
export const messageEvents = Event.implement(
|
|
29
|
+
MessagePosted,
|
|
30
|
+
Effect.gen(function* () {
|
|
31
|
+
const store = yield* MessageStore;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
check: (_input, cursor) =>
|
|
35
|
+
Effect.gen(function* () {
|
|
36
|
+
const room = yield* CurrentRoom;
|
|
37
|
+
const last = yield* store.last(room);
|
|
38
|
+
|
|
39
|
+
const after = Option.filter(
|
|
40
|
+
positionOf(store.epoch, cursor),
|
|
41
|
+
(id) => id >= 0 && id <= last
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
// No cursor starts after the room's last message. One this room never gave, such as
|
|
45
|
+
// one from before a restart, is answered `truncated` from there: the page learns its
|
|
46
|
+
// messages may be another run's, and reads the room again with `readMessages`.
|
|
47
|
+
if (Option.isNone(after)) {
|
|
48
|
+
return {
|
|
49
|
+
cursor: `${store.epoch}:${last}`,
|
|
50
|
+
events: [],
|
|
51
|
+
truncated: cursor !== null,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const messages = yield* store.read(room, after.value);
|
|
56
|
+
|
|
57
|
+
return {
|
|
58
|
+
cursor: `${store.epoch}:${messages.at(-1)?.id ?? after.value}`,
|
|
59
|
+
// A message's id counts within its room and the store's epoch, so the event's id
|
|
60
|
+
// names all three: a watch drops an id it has seen, and after a restart of the memory
|
|
61
|
+
// store `lobby:1` is another message.
|
|
62
|
+
events: messages.map((data) => ({
|
|
63
|
+
eventId: `${store.epoch}:${room}:${data.id}`,
|
|
64
|
+
data,
|
|
65
|
+
})),
|
|
66
|
+
};
|
|
67
|
+
}),
|
|
68
|
+
// A notify for one room wakes that room's pages alone: the key is the room the watch's
|
|
69
|
+
// header names, read as `check` reads it.
|
|
70
|
+
key: () => CurrentRoom,
|
|
71
|
+
};
|
|
72
|
+
}),
|
|
73
|
+
{ authorize }
|
|
74
|
+
);
|
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
import { Effect } from "effect";
|
|
2
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
2
|
+
import * as Event from "@gjermundgaraba/effect-actions/Event";
|
|
3
|
+
import { Effect } from "effect";
|
|
4
|
+
|
|
3
5
|
import { CurrentMember } from "../identity/Identity.js";
|
|
4
6
|
import { authorize } from "./authorize.js";
|
|
5
|
-
import { ListRooms, Messages } from "./Chat.js";
|
|
7
|
+
import { ListRooms, MessagePosted, Messages } from "./Chat.js";
|
|
6
8
|
import { CurrentRoom } from "./room.js";
|
|
7
9
|
import { MessageStore } from "./store.js";
|
|
8
10
|
|
|
@@ -13,7 +15,7 @@ export const rooms = Action.implement(
|
|
|
13
15
|
const store = yield* MessageStore;
|
|
14
16
|
|
|
15
17
|
return () => Effect.map(store.rooms, (found) => ({ rooms: found }));
|
|
16
|
-
})
|
|
18
|
+
})
|
|
17
19
|
);
|
|
18
20
|
|
|
19
21
|
// The builder runs once, at startup: it is where a handler gets its startup services. The
|
|
@@ -22,6 +24,7 @@ export const messages = Action.implement(
|
|
|
22
24
|
Messages,
|
|
23
25
|
Effect.gen(function* () {
|
|
24
26
|
const store = yield* MessageStore;
|
|
27
|
+
const events = yield* Event.Events;
|
|
25
28
|
|
|
26
29
|
return {
|
|
27
30
|
readMessages: ({ after }) =>
|
|
@@ -30,12 +33,17 @@ export const messages = Action.implement(
|
|
|
30
33
|
|
|
31
34
|
return { messages: yield* store.read(room, after) };
|
|
32
35
|
}),
|
|
36
|
+
// The message is in the store, which `MessagePosted`'s check reads: notifying wakes the
|
|
37
|
+
// pages watching this room to read it now.
|
|
33
38
|
postMessage: ({ text }) =>
|
|
34
39
|
Effect.gen(function* () {
|
|
35
40
|
const room = yield* CurrentRoom;
|
|
36
41
|
const member = yield* CurrentMember;
|
|
42
|
+
const message = yield* store.post(room, member.id, text);
|
|
43
|
+
|
|
44
|
+
yield* events.notify(MessagePosted, { key: room });
|
|
37
45
|
|
|
38
|
-
return
|
|
46
|
+
return message;
|
|
39
47
|
}),
|
|
40
48
|
deleteMessage: ({ id }) =>
|
|
41
49
|
Effect.gen(function* () {
|
|
@@ -46,5 +54,5 @@ export const messages = Action.implement(
|
|
|
46
54
|
}),
|
|
47
55
|
};
|
|
48
56
|
}),
|
|
49
|
-
{ authorize }
|
|
57
|
+
{ authorize }
|
|
50
58
|
);
|
|
@@ -1,23 +1,27 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect";
|
|
2
2
|
import { RpcMiddleware } from "effect/rpc";
|
|
3
|
+
|
|
3
4
|
import { defaultRoom, roomHeader } from "./Chat.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* The room a message is about. A request-time service: handlers and `authorize` read it per
|
|
7
8
|
* call, and it has no Layer, because no one value of it is right for a whole server.
|
|
8
9
|
*/
|
|
9
|
-
export class CurrentRoom extends Context.Service<CurrentRoom, string>()(
|
|
10
|
+
export class CurrentRoom extends Context.Service<CurrentRoom, string>()(
|
|
11
|
+
"live-rpc/CurrentRoom"
|
|
12
|
+
) {}
|
|
10
13
|
|
|
11
14
|
// Native Effect RPC middleware, declared as providing `CurrentRoom`. Listed in
|
|
12
15
|
// `ActionRpc.layer`'s `middleware`, it runs once per message.
|
|
13
|
-
export class ResolveRoom extends RpcMiddleware.Service<
|
|
14
|
-
|
|
15
|
-
|
|
16
|
+
export class ResolveRoom extends RpcMiddleware.Service<
|
|
17
|
+
ResolveRoom,
|
|
18
|
+
{ provides: CurrentRoom }
|
|
19
|
+
>()("live-rpc/ResolveRoom") {}
|
|
16
20
|
|
|
17
21
|
// The middleware's implementation. It gets the message's headers, which over a WebSocket are
|
|
18
22
|
// the upgrade request's merged with the message's own, and runs the rest of the call with the
|
|
19
23
|
// room they name, or the default room when they name none. It refuses nothing: whether the
|
|
20
24
|
// caller may be in that room is `authorize`'s rule.
|
|
21
25
|
export const resolveRoom = Layer.succeed(ResolveRoom, (call, { headers }) =>
|
|
22
|
-
Effect.provideService(call, CurrentRoom, headers[roomHeader] ?? defaultRoom)
|
|
26
|
+
Effect.provideService(call, CurrentRoom, headers[roomHeader] ?? defaultRoom)
|
|
23
27
|
);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Context, Effect, Layer } from "effect";
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { MessageNotFound, NotAuthor } from "./Chat.js";
|
|
4
|
+
import type { Message } from "./Chat.js";
|
|
3
5
|
|
|
4
6
|
/**
|
|
5
7
|
* Where messages live, by room. That only its author deletes a message is checked here, in the
|
|
@@ -8,34 +10,62 @@ import { type Message, MessageNotFound, NotAuthor } from "./Chat.js";
|
|
|
8
10
|
export class MessageStore extends Context.Service<
|
|
9
11
|
MessageStore,
|
|
10
12
|
{
|
|
11
|
-
|
|
12
|
-
readonly
|
|
13
|
-
readonly
|
|
13
|
+
/** The store's epoch: a message's id is unique within it, and a restarted memory store starts another. */
|
|
14
|
+
readonly epoch: string;
|
|
15
|
+
readonly rooms: Effect.Effect<
|
|
16
|
+
ReadonlyArray<{ name: string; messages: number }>
|
|
17
|
+
>;
|
|
18
|
+
readonly read: (
|
|
19
|
+
room: string,
|
|
20
|
+
after: number
|
|
21
|
+
) => Effect.Effect<readonly Message[]>;
|
|
22
|
+
/** The id of the room's last message posted, kept or deleted: 0 for an empty room. */
|
|
23
|
+
readonly last: (room: string) => Effect.Effect<number>;
|
|
24
|
+
readonly post: (
|
|
25
|
+
room: string,
|
|
26
|
+
author: string,
|
|
27
|
+
text: string
|
|
28
|
+
) => Effect.Effect<Message>;
|
|
14
29
|
readonly remove: (
|
|
15
30
|
room: string,
|
|
16
31
|
author: string,
|
|
17
|
-
id: number
|
|
32
|
+
id: number
|
|
18
33
|
) => Effect.Effect<void, MessageNotFound | NotAuthor>;
|
|
19
34
|
}
|
|
20
35
|
>()("live-rpc/MessageStore") {
|
|
21
|
-
// DEMO ONLY: memory, lost when the process stops
|
|
22
|
-
// service, chosen by the
|
|
36
|
+
// DEMO ONLY: memory, lost when the process stops, its ids then counting from 1 again, so each
|
|
37
|
+
// start is another epoch. A real store is another Layer of this service, chosen by the
|
|
38
|
+
// entrypoint, whose ids never repeat.
|
|
23
39
|
static readonly layerMemory = Layer.sync(MessageStore, () => {
|
|
24
|
-
const
|
|
40
|
+
const epoch = crypto.randomUUID();
|
|
41
|
+
|
|
42
|
+
const rooms = new Map<
|
|
43
|
+
string,
|
|
44
|
+
{ last: number; messages: readonly Message[] }
|
|
45
|
+
>();
|
|
25
46
|
|
|
26
47
|
const room = (name: string) => rooms.get(name) ?? { last: 0, messages: [] };
|
|
27
48
|
|
|
28
49
|
return MessageStore.of({
|
|
50
|
+
epoch,
|
|
29
51
|
rooms: Effect.sync(() =>
|
|
30
|
-
Array.from(rooms, ([name, { messages }]) => ({
|
|
52
|
+
Array.from(rooms, ([name, { messages }]) => ({
|
|
53
|
+
name,
|
|
54
|
+
messages: messages.length,
|
|
55
|
+
}))
|
|
31
56
|
),
|
|
32
|
-
read: (name, after) =>
|
|
57
|
+
read: (name, after) =>
|
|
58
|
+
Effect.sync(() => room(name).messages.filter(({ id }) => id > after)),
|
|
59
|
+
last: (name) => Effect.sync(() => room(name).last),
|
|
33
60
|
post: (name, author, text) =>
|
|
34
61
|
Effect.sync(() => {
|
|
35
62
|
const { last, messages } = room(name);
|
|
36
63
|
const message = { id: last + 1, author, text };
|
|
37
64
|
|
|
38
|
-
rooms.set(name, {
|
|
65
|
+
rooms.set(name, {
|
|
66
|
+
last: message.id,
|
|
67
|
+
messages: [...messages, message],
|
|
68
|
+
});
|
|
39
69
|
|
|
40
70
|
return message;
|
|
41
71
|
}),
|
|
@@ -43,11 +73,18 @@ export class MessageStore extends Context.Service<
|
|
|
43
73
|
const { last, messages } = room(name);
|
|
44
74
|
const message = messages.find((candidate) => candidate.id === id);
|
|
45
75
|
|
|
46
|
-
if (message === undefined)
|
|
76
|
+
if (message === undefined) {
|
|
77
|
+
return yield* new MessageNotFound({ id });
|
|
78
|
+
}
|
|
47
79
|
|
|
48
|
-
if (message.author !== author)
|
|
80
|
+
if (message.author !== author) {
|
|
81
|
+
return yield* new NotAuthor({ id });
|
|
82
|
+
}
|
|
49
83
|
|
|
50
|
-
rooms.set(name, {
|
|
84
|
+
rooms.set(name, {
|
|
85
|
+
last,
|
|
86
|
+
messages: messages.filter((kept) => kept !== message),
|
|
87
|
+
});
|
|
51
88
|
}),
|
|
52
89
|
});
|
|
53
90
|
});
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { NodeRuntime, NodeSocket } from "@effect/platform-node";
|
|
2
|
-
import { Config, Console, Effect, Layer } from "effect";
|
|
2
|
+
import { Config, Console, Effect, Layer, Stream } from "effect";
|
|
3
|
+
|
|
3
4
|
import { connection, openChat } from "./browser.js";
|
|
4
5
|
|
|
5
6
|
// Entrypoint: `node --import tsx examples/live-rpc/src/client.ts`, with the server running.
|
|
@@ -17,19 +18,35 @@ const visit = Effect.gen(function* () {
|
|
|
17
18
|
|
|
18
19
|
yield* Console.log("ada posts:", posted);
|
|
19
20
|
yield* Console.log("grace reads:", yield* grace.read(posted.id - 1));
|
|
21
|
+
// A watch from the last id grace held before the post: it sends the post, then would send
|
|
22
|
+
// each one after it, until taken.
|
|
23
|
+
yield* Console.log(
|
|
24
|
+
"grace watches:",
|
|
25
|
+
yield* grace.messages(posted.id - 1).pipe(Stream.take(1), Stream.runCollect)
|
|
26
|
+
);
|
|
20
27
|
yield* Console.log("grace removes it:", yield* grace.remove(posted.id));
|
|
21
28
|
yield* Console.log("ada removes it:", yield* ada.remove(posted.id));
|
|
22
|
-
yield* Console.log(
|
|
23
|
-
|
|
29
|
+
yield* Console.log(
|
|
30
|
+
"grace reads ops:",
|
|
31
|
+
yield* chat.room("ops", "grace").read(0)
|
|
32
|
+
);
|
|
33
|
+
yield* Console.log(
|
|
34
|
+
"an expired token reads:",
|
|
35
|
+
yield* chat.room("lobby", "expired").read(0)
|
|
36
|
+
);
|
|
24
37
|
});
|
|
25
38
|
|
|
26
39
|
const main = Effect.gen(function* () {
|
|
27
|
-
const url = yield* Config.String("CHAT_URL").pipe(
|
|
40
|
+
const url = yield* Config.String("CHAT_URL").pipe(
|
|
41
|
+
Config.withDefault("ws://127.0.0.1:3000/rpc")
|
|
42
|
+
);
|
|
28
43
|
|
|
29
44
|
// One socket for the whole visit, so both members' calls share it.
|
|
30
45
|
yield* visit.pipe(
|
|
31
46
|
Effect.scoped,
|
|
32
|
-
Effect.provide(
|
|
47
|
+
Effect.provide(
|
|
48
|
+
connection(url).pipe(Layer.provide(NodeSocket.layerWebSocketConstructor))
|
|
49
|
+
)
|
|
33
50
|
);
|
|
34
51
|
});
|
|
35
52
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Context } from "effect";
|
|
2
1
|
import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
|
|
2
|
+
import { Context } from "effect";
|
|
3
3
|
|
|
4
4
|
export interface Member {
|
|
5
5
|
readonly id: string;
|
|
6
|
-
readonly rooms:
|
|
6
|
+
readonly rooms: readonly string[];
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
/**
|
|
@@ -11,7 +11,7 @@ export interface Member {
|
|
|
11
11
|
* provides it for each message, and a test calling in process provides it itself.
|
|
12
12
|
*/
|
|
13
13
|
export class CurrentMember extends Context.Service<CurrentMember, Member>()(
|
|
14
|
-
"live-rpc/CurrentMember"
|
|
14
|
+
"live-rpc/CurrentMember"
|
|
15
15
|
) {}
|
|
16
16
|
|
|
17
17
|
// How a remote caller proves it is CurrentMember: a bearer token, the default scheme. Clients
|
|
@@ -1,20 +1,24 @@
|
|
|
1
|
-
import { Effect, Redacted } from "effect";
|
|
2
1
|
import * as Action from "@gjermundgaraba/effect-actions/Action";
|
|
3
2
|
import * as Authentication from "@gjermundgaraba/effect-actions/Authentication";
|
|
3
|
+
import { Effect, Redacted } from "effect";
|
|
4
|
+
|
|
4
5
|
import { Login } from "./Identity.js";
|
|
5
6
|
import { members } from "./members.js";
|
|
6
7
|
|
|
7
|
-
const isToken = (token: string): token is keyof typeof members =>
|
|
8
|
+
const isToken = (token: string): token is keyof typeof members =>
|
|
9
|
+
Object.hasOwn(members, token);
|
|
8
10
|
|
|
9
11
|
// DEMO ONLY: a token is a member's name. A real verifier checks a signed token with your
|
|
10
12
|
// authorization server's library, its audience included. An unknown token is the built-in
|
|
11
13
|
// `Unauthenticated`, which every client decodes.
|
|
12
|
-
const verify = (token: Redacted.Redacted
|
|
14
|
+
const verify = (token: Redacted.Redacted) => {
|
|
13
15
|
const name = Redacted.value(token);
|
|
14
16
|
|
|
15
17
|
return isToken(name)
|
|
16
18
|
? Effect.succeed(members[name])
|
|
17
|
-
: Effect.fail(
|
|
19
|
+
: Effect.fail(
|
|
20
|
+
new Action.Unauthenticated({ message: "Unknown demo token." })
|
|
21
|
+
);
|
|
18
22
|
};
|
|
19
23
|
|
|
20
24
|
// Server only: `Login`'s provider. Over RPC it runs for every message of a protected rpc, not
|
|
@@ -1,34 +1,80 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
1
|
+
import type * as Event from "@gjermundgaraba/effect-actions/Event";
|
|
2
|
+
import { Context, Effect, Exit, Layer, ManagedRuntime, Stream } from "effect";
|
|
3
|
+
import type { RpcClient } from "effect/rpc";
|
|
3
4
|
import { Socket } from "effect/socket";
|
|
5
|
+
|
|
4
6
|
import { connection, openChat } from "./browser.js";
|
|
7
|
+
import type { MessagePosted } from "./chat/Chat.js";
|
|
5
8
|
|
|
6
9
|
// The chat for a page that is not one Effect program: the same client, each call a promise.
|
|
7
10
|
// A `ManagedRuntime` keeps the protocol's layer, and so the socket, between calls. It builds
|
|
8
11
|
// the layer on the first call, which is when the socket opens, and `close` releases it. A
|
|
9
12
|
// failure `openChat` does not handle, such as `InvalidInput`, rejects the promise.
|
|
10
|
-
class Chat extends Context.Service<Chat, Effect.Success<typeof openChat>>()(
|
|
13
|
+
class Chat extends Context.Service<Chat, Effect.Success<typeof openChat>>()(
|
|
14
|
+
"live-rpc/Chat"
|
|
15
|
+
) {}
|
|
11
16
|
|
|
12
17
|
export const chatOver = <E>(protocol: Layer.Layer<RpcClient.Protocol, E>) => {
|
|
13
|
-
const runtime = ManagedRuntime.make(
|
|
18
|
+
const runtime = ManagedRuntime.make(
|
|
19
|
+
Layer.effect(Chat, openChat).pipe(Layer.provide(protocol))
|
|
20
|
+
);
|
|
14
21
|
|
|
15
|
-
const call = <A, E2>(
|
|
16
|
-
|
|
22
|
+
const call = async <A, E2>(
|
|
23
|
+
use: (chat: Chat["Service"]) => Effect.Effect<A, E2>
|
|
24
|
+
) => await runtime.runPromise(Effect.flatMap(Chat, use));
|
|
17
25
|
|
|
18
26
|
return {
|
|
19
|
-
rooms: () => call((chat) => chat.rooms),
|
|
27
|
+
rooms: async () => await call((chat) => chat.rooms),
|
|
20
28
|
|
|
21
29
|
room: (name: string, token: string) => ({
|
|
22
|
-
read: (after: number) =>
|
|
23
|
-
|
|
24
|
-
|
|
30
|
+
read: async (after: number) =>
|
|
31
|
+
await call((chat) => chat.room(name, token).read(after)),
|
|
32
|
+
post: async (text: string) =>
|
|
33
|
+
await call((chat) => chat.room(name, token).post(text)),
|
|
34
|
+
remove: async (id: number) =>
|
|
35
|
+
await call((chat) => chat.room(name, token).remove(id)),
|
|
36
|
+
// Calls `receive` with each message posted after `after`, the id of the last message the
|
|
37
|
+
// page holds, and each gap, as `messages` streams them, until the page calls the function
|
|
38
|
+
// it returns. `ended` is told why it stopped: a refusal, such as a token that expired by
|
|
39
|
+
// the time the watch connected again, or anything else, the page's own call included.
|
|
40
|
+
watch: (
|
|
41
|
+
after: number,
|
|
42
|
+
receive: (received: Event.Received<typeof MessagePosted>) => void,
|
|
43
|
+
ended: (why: "signed-out" | "not-a-member" | "stopped") => void
|
|
44
|
+
) =>
|
|
45
|
+
runtime.runCallback(
|
|
46
|
+
Effect.flatMap(Chat, (chat) =>
|
|
47
|
+
Stream.runForEach(
|
|
48
|
+
chat.room(name, token).messages(after),
|
|
49
|
+
(received) =>
|
|
50
|
+
Effect.sync(() => {
|
|
51
|
+
receive(received);
|
|
52
|
+
})
|
|
53
|
+
)
|
|
54
|
+
).pipe(
|
|
55
|
+
Effect.as("stopped" as const),
|
|
56
|
+
Effect.catchTags({
|
|
57
|
+
Unauthenticated: () => Effect.succeed("signed-out" as const),
|
|
58
|
+
Forbidden: () => Effect.succeed("not-a-member" as const),
|
|
59
|
+
})
|
|
60
|
+
),
|
|
61
|
+
{
|
|
62
|
+
onExit: (exit) => {
|
|
63
|
+
ended(Exit.isSuccess(exit) ? exit.value : "stopped");
|
|
64
|
+
},
|
|
65
|
+
}
|
|
66
|
+
),
|
|
25
67
|
}),
|
|
26
68
|
|
|
27
|
-
close: () =>
|
|
69
|
+
close: async () => {
|
|
70
|
+
await runtime.dispose();
|
|
71
|
+
},
|
|
28
72
|
};
|
|
29
73
|
};
|
|
30
74
|
|
|
31
75
|
// What a page calls: `const chat = connect("wss://chat.example/rpc")`, on the browser's own
|
|
32
76
|
// WebSocket, then `await chat.room("lobby", token).post("Hello")`.
|
|
33
77
|
export const connect = (url: string) =>
|
|
34
|
-
chatOver(
|
|
78
|
+
chatOver(
|
|
79
|
+
connection(url).pipe(Layer.provide(Socket.layerWebSocketConstructorGlobal))
|
|
80
|
+
);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
-
import { Layer } from "effect";
|
|
2
1
|
import * as ActionRpc from "@gjermundgaraba/effect-actions/ActionRpc";
|
|
2
|
+
import { Layer } from "effect";
|
|
3
|
+
|
|
4
|
+
import { messageEvents } from "./chat/events.js";
|
|
3
5
|
import { messages, rooms } from "./chat/handlers.js";
|
|
4
6
|
import { ResolveRoom, resolveRoom } from "./chat/room.js";
|
|
5
7
|
import { authenticate } from "./identity/verifier.js";
|
|
@@ -8,14 +10,16 @@ import { Rpc } from "./Rpc.js";
|
|
|
8
10
|
// The binding's rpcs on Effect's own RpcServer, the protected ones behind `Login`'s verifier;
|
|
9
11
|
// `listRooms` needs no token.
|
|
10
12
|
//
|
|
11
|
-
// `ResolveRoom`, listed in `middleware`, provides `CurrentRoom`, which the handlers
|
|
12
|
-
// `authorize` read
|
|
13
|
-
//
|
|
14
|
-
// requiring it,
|
|
13
|
+
// `ResolveRoom`, listed in `middleware`, provides `CurrentRoom`, which the handlers, the event's
|
|
14
|
+
// source and `authorize` read; on a watch it runs as the watch starts, and each time it connects
|
|
15
|
+
// again. Only something that runs per message can, since each message names its own room:
|
|
16
|
+
// `Layer.succeed(CurrentRoom, "lobby")` provided here would leave the layer still requiring it,
|
|
17
|
+
// in its type.
|
|
15
18
|
//
|
|
16
|
-
// It still requires
|
|
19
|
+
// It still requires four things, each chosen by whoever serves it. The protocol and its
|
|
17
20
|
// serialization: server.ts gives it a WebSocket, and a test Effect's HTTP protocol, which
|
|
18
|
-
// answers in memory.
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
21
|
+
// answers in memory. `MessageStore`, and `Event.Events`, which `postMessage` notifies and the
|
|
22
|
+
// watches wait on: one of each, provided around the whole server.
|
|
23
|
+
export const rpc = ActionRpc.layer(Rpc, [rooms, messages, messageEvents], {
|
|
24
|
+
middleware: [ResolveRoom],
|
|
25
|
+
}).pipe(Layer.provide([resolveRoom, authenticate]));
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { createServer } from "node:http";
|
|
2
|
+
|
|
2
3
|
import { NodeHttpServer, NodeRuntime } from "@effect/platform-node";
|
|
4
|
+
import * as Event from "@gjermundgaraba/effect-actions/Event";
|
|
3
5
|
import { Config, Effect, Layer } from "effect";
|
|
4
6
|
import { HttpRouter } from "effect/http";
|
|
5
7
|
import { RpcSerialization, RpcServer } from "effect/rpc";
|
|
8
|
+
|
|
6
9
|
import { MessageStore } from "./chat/store.js";
|
|
7
10
|
import { rpc } from "./routes.js";
|
|
8
11
|
|
|
@@ -15,13 +18,16 @@ import { rpc } from "./routes.js";
|
|
|
15
18
|
// serialization.
|
|
16
19
|
const socket = rpc.pipe(
|
|
17
20
|
Layer.provide(RpcServer.layerProtocolWebsocket({ path: "/rpc" })),
|
|
18
|
-
Layer.provide(RpcSerialization.layerJson)
|
|
21
|
+
Layer.provide(RpcSerialization.layerJson)
|
|
19
22
|
);
|
|
20
23
|
|
|
21
24
|
const server = Layer.unwrap(
|
|
22
25
|
Effect.gen(function* () {
|
|
23
26
|
// The loopback default serves this machine alone; HOST=0.0.0.0 lets others reach it.
|
|
24
|
-
const host = yield* Config.String("HOST").pipe(
|
|
27
|
+
const host = yield* Config.String("HOST").pipe(
|
|
28
|
+
Config.withDefault("127.0.0.1")
|
|
29
|
+
);
|
|
30
|
+
|
|
25
31
|
const port = yield* Config.Port("PORT").pipe(Config.withDefault(3000));
|
|
26
32
|
|
|
27
33
|
return HttpRouter.serve(socket).pipe(
|
|
@@ -33,13 +39,17 @@ const server = Layer.unwrap(
|
|
|
33
39
|
// does not bound it: Node's WebSocket server reads one of up to 100 MiB whole unless
|
|
34
40
|
// told otherwise. A chat message is short, and a larger one closes the socket (1009).
|
|
35
41
|
websocket: { maxPayload: 64 * 1024 },
|
|
36
|
-
})
|
|
42
|
+
})
|
|
37
43
|
),
|
|
38
44
|
// DEMO ONLY: messages live in this process's memory; a deployment provides a store that
|
|
39
45
|
// lasts.
|
|
40
46
|
Layer.provide(MessageStore.layerMemory),
|
|
47
|
+
// The pages watching a room are this process's sockets, so what wakes them lives in its
|
|
48
|
+
// memory too: one for the whole server. Several processes behind a balancer would each
|
|
49
|
+
// wake only their own pages, the others reading a post at their next read, 5 seconds on.
|
|
50
|
+
Layer.provide(Event.layerMemory)
|
|
41
51
|
);
|
|
42
|
-
})
|
|
52
|
+
})
|
|
43
53
|
);
|
|
44
54
|
|
|
45
|
-
Layer.launch(server).pipe(NodeRuntime.runMain);
|
|
55
|
+
Layer.launch(server).pipe(NodeRuntime.runMain());
|