@rpgjs/map-builder 0.0.0-stage → 5.0.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/LICENSE +19 -0
- package/README.md +341 -2
- package/dist/authority/authority.d.ts +118 -0
- package/dist/authority/authority.d.ts.map +1 -0
- package/dist/authority/authority.js +289 -0
- package/dist/authority/authority.js.map +1 -0
- package/dist/authority/index.d.ts +3 -0
- package/dist/authority/index.d.ts.map +1 -0
- package/dist/authority/index.js +3 -0
- package/dist/authority/store.d.ts +36 -0
- package/dist/authority/store.d.ts.map +1 -0
- package/dist/authority/store.js +35 -0
- package/dist/authority/store.js.map +1 -0
- package/dist/client/index.d.ts +72 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +200 -0
- package/dist/client/index.js.map +1 -0
- package/dist/embed/client.d.ts +85 -0
- package/dist/embed/client.d.ts.map +1 -0
- package/dist/embed/client.js +255 -0
- package/dist/embed/client.js.map +1 -0
- package/dist/embed/host.d.ts +112 -0
- package/dist/embed/host.d.ts.map +1 -0
- package/dist/embed/host.js +278 -0
- package/dist/embed/host.js.map +1 -0
- package/dist/embed/index.d.ts +4 -0
- package/dist/embed/index.d.ts.map +1 -0
- package/dist/embed/index.js +4 -0
- package/dist/embed/protocol.d.ts +137 -0
- package/dist/embed/protocol.d.ts.map +1 -0
- package/dist/embed/protocol.js +161 -0
- package/dist/embed/protocol.js.map +1 -0
- package/dist/features/index.d.ts +66 -0
- package/dist/features/index.d.ts.map +1 -0
- package/dist/features/index.js +157 -0
- package/dist/features/index.js.map +1 -0
- package/dist/features/skin.d.ts +9 -0
- package/dist/features/skin.d.ts.map +1 -0
- package/dist/features/skin.js +43 -0
- package/dist/features/skin.js.map +1 -0
- package/dist/features/terrain.d.ts +66 -0
- package/dist/features/terrain.d.ts.map +1 -0
- package/dist/features/terrain.js +291 -0
- package/dist/features/terrain.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +51 -0
- package/dist/interaction/builtin-intents.d.ts +9 -0
- package/dist/interaction/builtin-intents.d.ts.map +1 -0
- package/dist/interaction/builtin-intents.js +61 -0
- package/dist/interaction/builtin-intents.js.map +1 -0
- package/dist/interaction/catalog.d.ts +24 -0
- package/dist/interaction/catalog.d.ts.map +1 -0
- package/dist/interaction/catalog.js +20 -0
- package/dist/interaction/catalog.js.map +1 -0
- package/dist/interaction/controller.d.ts +137 -0
- package/dist/interaction/controller.d.ts.map +1 -0
- package/dist/interaction/controller.js +215 -0
- package/dist/interaction/controller.js.map +1 -0
- package/dist/interaction/index.d.ts +14 -0
- package/dist/interaction/index.d.ts.map +1 -0
- package/dist/interaction/index.js +13 -0
- package/dist/interaction/input.d.ts +26 -0
- package/dist/interaction/input.d.ts.map +1 -0
- package/dist/interaction/input.js +33 -0
- package/dist/interaction/input.js.map +1 -0
- package/dist/interaction/intents.d.ts +80 -0
- package/dist/interaction/intents.d.ts.map +1 -0
- package/dist/interaction/keyboard.d.ts +26 -0
- package/dist/interaction/keyboard.d.ts.map +1 -0
- package/dist/interaction/keyboard.js +32 -0
- package/dist/interaction/keyboard.js.map +1 -0
- package/dist/interaction/picking.d.ts +33 -0
- package/dist/interaction/picking.d.ts.map +1 -0
- package/dist/interaction/picking.js +35 -0
- package/dist/interaction/picking.js.map +1 -0
- package/dist/interaction/pipeline.d.ts +45 -0
- package/dist/interaction/pipeline.d.ts.map +1 -0
- package/dist/interaction/pipeline.js +26 -0
- package/dist/interaction/pipeline.js.map +1 -0
- package/dist/interaction/placement.d.ts +76 -0
- package/dist/interaction/placement.d.ts.map +1 -0
- package/dist/interaction/placement.js +238 -0
- package/dist/interaction/placement.js.map +1 -0
- package/dist/interaction/selection-tool.d.ts +29 -0
- package/dist/interaction/selection-tool.d.ts.map +1 -0
- package/dist/interaction/selection-tool.js +190 -0
- package/dist/interaction/selection-tool.js.map +1 -0
- package/dist/interaction/steps.d.ts +62 -0
- package/dist/interaction/steps.d.ts.map +1 -0
- package/dist/interaction/steps.js +126 -0
- package/dist/interaction/steps.js.map +1 -0
- package/dist/interaction/store.d.ts +10 -0
- package/dist/interaction/store.d.ts.map +1 -0
- package/dist/interaction/store.js +28 -0
- package/dist/interaction/store.js.map +1 -0
- package/dist/interaction/tools.d.ts +26 -0
- package/dist/interaction/tools.d.ts.map +1 -0
- package/dist/interaction/tools.js +41 -0
- package/dist/interaction/tools.js.map +1 -0
- package/dist/model/assets.d.ts +36 -0
- package/dist/model/assets.d.ts.map +1 -0
- package/dist/model/assets.js +18 -0
- package/dist/model/assets.js.map +1 -0
- package/dist/model/committer.d.ts +66 -0
- package/dist/model/committer.d.ts.map +1 -0
- package/dist/model/committer.js +38 -0
- package/dist/model/committer.js.map +1 -0
- package/dist/model/core-patches.d.ts +2 -0
- package/dist/model/core-patches.d.ts.map +1 -0
- package/dist/model/core-patches.js +193 -0
- package/dist/model/core-patches.js.map +1 -0
- package/dist/model/define-map.d.ts +52 -0
- package/dist/model/define-map.d.ts.map +1 -0
- package/dist/model/define-map.js +66 -0
- package/dist/model/define-map.js.map +1 -0
- package/dist/model/definition.d.ts +57 -0
- package/dist/model/definition.d.ts.map +1 -0
- package/dist/model/definition.js +6 -0
- package/dist/model/definition.js.map +1 -0
- package/dist/model/errors.d.ts +21 -0
- package/dist/model/errors.d.ts.map +1 -0
- package/dist/model/errors.js +14 -0
- package/dist/model/errors.js.map +1 -0
- package/dist/model/history.d.ts +43 -0
- package/dist/model/history.d.ts.map +1 -0
- package/dist/model/history.js +141 -0
- package/dist/model/history.js.map +1 -0
- package/dist/model/index.d.ts +14 -0
- package/dist/model/index.d.ts.map +1 -0
- package/dist/model/index.js +14 -0
- package/dist/model/json.d.ts +15 -0
- package/dist/model/json.d.ts.map +1 -0
- package/dist/model/json.js +35 -0
- package/dist/model/json.js.map +1 -0
- package/dist/model/model.d.ts +68 -0
- package/dist/model/model.d.ts.map +1 -0
- package/dist/model/model.js +170 -0
- package/dist/model/model.js.map +1 -0
- package/dist/model/parse.d.ts +24 -0
- package/dist/model/parse.d.ts.map +1 -0
- package/dist/model/parse.js +165 -0
- package/dist/model/parse.js.map +1 -0
- package/dist/model/patches.d.ts +100 -0
- package/dist/model/patches.d.ts.map +1 -0
- package/dist/model/patches.js +21 -0
- package/dist/model/patches.js.map +1 -0
- package/dist/model/result.d.ts +22 -0
- package/dist/model/result.d.ts.map +1 -0
- package/dist/model/result.js +10 -0
- package/dist/model/result.js.map +1 -0
- package/dist/model/source.d.ts +52 -0
- package/dist/model/source.d.ts.map +1 -0
- package/dist/model/source.js +75 -0
- package/dist/model/source.js.map +1 -0
- package/dist/protocol/index.d.ts +92 -0
- package/dist/protocol/index.d.ts.map +1 -0
- package/dist/protocol/index.js +106 -0
- package/dist/protocol/index.js.map +1 -0
- package/dist/remote/committer.d.ts +43 -0
- package/dist/remote/committer.d.ts.map +1 -0
- package/dist/remote/committer.js +206 -0
- package/dist/remote/committer.js.map +1 -0
- package/dist/remote/index.d.ts +4 -0
- package/dist/remote/index.d.ts.map +1 -0
- package/dist/remote/index.js +4 -0
- package/dist/remote/join.d.ts +30 -0
- package/dist/remote/join.d.ts.map +1 -0
- package/dist/remote/join.js +36 -0
- package/dist/remote/join.js.map +1 -0
- package/dist/remote/memory-transport.d.ts +20 -0
- package/dist/remote/memory-transport.d.ts.map +1 -0
- package/dist/remote/memory-transport.js +48 -0
- package/dist/remote/memory-transport.js.map +1 -0
- package/dist/renderer/effects.d.ts +52 -0
- package/dist/renderer/effects.d.ts.map +1 -0
- package/dist/renderer/effects.js +111 -0
- package/dist/renderer/effects.js.map +1 -0
- package/dist/renderer/feature.d.ts +79 -0
- package/dist/renderer/feature.d.ts.map +1 -0
- package/dist/renderer/feature.js +50 -0
- package/dist/renderer/feature.js.map +1 -0
- package/dist/renderer/geometry.d.ts +59 -0
- package/dist/renderer/geometry.d.ts.map +1 -0
- package/dist/renderer/geometry.js +114 -0
- package/dist/renderer/geometry.js.map +1 -0
- package/dist/renderer/index.d.ts +7 -0
- package/dist/renderer/index.d.ts.map +1 -0
- package/dist/renderer/index.js +7 -0
- package/dist/renderer/scene.d.ts +77 -0
- package/dist/renderer/scene.d.ts.map +1 -0
- package/dist/renderer/scene.js +125 -0
- package/dist/renderer/scene.js.map +1 -0
- package/dist/renderer/textures.d.ts +20 -0
- package/dist/renderer/textures.d.ts.map +1 -0
- package/dist/renderer/textures.js +61 -0
- package/dist/renderer/textures.js.map +1 -0
- package/dist/renderer/view.d.ts +139 -0
- package/dist/renderer/view.d.ts.map +1 -0
- package/dist/renderer/view.js +349 -0
- package/dist/renderer/view.js.map +1 -0
- package/dist/rpgjs/shared.d.ts +41 -0
- package/dist/rpgjs/shared.d.ts.map +1 -0
- package/dist/rpgjs/shared.js +77 -0
- package/dist/rpgjs/shared.js.map +1 -0
- package/dist/server/index.d.ts +64 -0
- package/dist/server/index.d.ts.map +1 -0
- package/dist/server/index.js +205 -0
- package/dist/server/index.js.map +1 -0
- package/dist/server/storage.d.ts +16 -0
- package/dist/server/storage.d.ts.map +1 -0
- package/dist/server/storage.js +52 -0
- package/dist/server/storage.js.map +1 -0
- package/dist/skins/default.d.ts +27 -0
- package/dist/skins/default.d.ts.map +1 -0
- package/dist/skins/default.js +144 -0
- package/dist/skins/default.js.map +1 -0
- package/dist/skins/frames.d.ts +19 -0
- package/dist/skins/frames.d.ts.map +1 -0
- package/dist/skins/frames.js +49 -0
- package/dist/skins/frames.js.map +1 -0
- package/dist/skins/game.d.ts +21 -0
- package/dist/skins/game.d.ts.map +1 -0
- package/dist/skins/game.js +21 -0
- package/dist/skins/game.js.map +1 -0
- package/dist/skins/index.d.ts +5 -0
- package/dist/skins/index.d.ts.map +1 -0
- package/dist/skins/index.js +5 -0
- package/dist/skins/types.d.ts +46 -0
- package/dist/skins/types.d.ts.map +1 -0
- package/dist/skins/types.js +12 -0
- package/dist/skins/types.js.map +1 -0
- package/dist/terrain/data.d.ts +10 -0
- package/dist/terrain/data.d.ts.map +1 -0
- package/dist/terrain/data.js +83 -0
- package/dist/terrain/data.js.map +1 -0
- package/dist/terrain/define.d.ts +72 -0
- package/dist/terrain/define.d.ts.map +1 -0
- package/dist/terrain/define.js +91 -0
- package/dist/terrain/define.js.map +1 -0
- package/dist/terrain/editing.d.ts +54 -0
- package/dist/terrain/editing.d.ts.map +1 -0
- package/dist/terrain/editing.js +169 -0
- package/dist/terrain/editing.js.map +1 -0
- package/dist/terrain/index.d.ts +10 -0
- package/dist/terrain/index.d.ts.map +1 -0
- package/dist/terrain/index.js +10 -0
- package/dist/terrain/kernel.d.ts +57 -0
- package/dist/terrain/kernel.d.ts.map +1 -0
- package/dist/terrain/kernel.js +198 -0
- package/dist/terrain/kernel.js.map +1 -0
- package/dist/terrain/mask.d.ts +37 -0
- package/dist/terrain/mask.d.ts.map +1 -0
- package/dist/terrain/mask.js +244 -0
- package/dist/terrain/mask.js.map +1 -0
- package/dist/terrain/patches.d.ts +32 -0
- package/dist/terrain/patches.d.ts.map +1 -0
- package/dist/terrain/patches.js +135 -0
- package/dist/terrain/patches.js.map +1 -0
- package/dist/terrain/rle.d.ts +4 -0
- package/dist/terrain/rle.d.ts.map +1 -0
- package/dist/terrain/rle.js +68 -0
- package/dist/terrain/rle.js.map +1 -0
- package/dist/terrain/scheduler.d.ts +46 -0
- package/dist/terrain/scheduler.d.ts.map +1 -0
- package/dist/terrain/scheduler.js +148 -0
- package/dist/terrain/scheduler.js.map +1 -0
- package/dist/terrain/types.d.ts +110 -0
- package/dist/terrain/types.d.ts.map +1 -0
- package/dist/terrain/types.js +11 -0
- package/dist/terrain/types.js.map +1 -0
- package/dist/terrain-render/backend.d.ts +66 -0
- package/dist/terrain-render/backend.d.ts.map +1 -0
- package/dist/terrain-render/backend.js +96 -0
- package/dist/terrain-render/backend.js.map +1 -0
- package/dist/terrain-render/host.d.ts +21 -0
- package/dist/terrain-render/host.d.ts.map +1 -0
- package/dist/terrain-render/host.js +62 -0
- package/dist/terrain-render/host.js.map +1 -0
- package/dist/terrain-render/index.d.ts +3 -0
- package/dist/terrain-render/index.d.ts.map +1 -0
- package/dist/terrain-render/index.js +3 -0
- package/dist/terrain-render/worker.d.ts +2 -0
- package/dist/terrain-render/worker.d.ts.map +1 -0
- package/dist/terrain-render/worker.js +34 -0
- package/dist/terrain-render/worker.js.map +1 -0
- package/package.json +124 -3
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { isJsonValue, isPlainObject } from "../model/json.js";
|
|
2
|
+
//#region src/protocol/index.ts
|
|
3
|
+
/**
|
|
4
|
+
* Messages between a map client and the authority of that map. They are plain JSON, so
|
|
5
|
+
* any transport carries them: a WebSocket, an RPGJS socket event, a Worker message.
|
|
6
|
+
*
|
|
7
|
+
* The client asks (`intent`), the authority decides and tells everybody (`patches`).
|
|
8
|
+
* A client never sends patches unless the authority trusts it.
|
|
9
|
+
*/
|
|
10
|
+
var MAP_BUILDER_EVENT = "map-builder";
|
|
11
|
+
/** What an authority refuses to read, to keep a client from making it work too hard. */
|
|
12
|
+
var PROTOCOL_LIMITS = {
|
|
13
|
+
/** Intents in one request. */
|
|
14
|
+
maxIntents: 64,
|
|
15
|
+
/** Patches in one trusted request. */
|
|
16
|
+
maxPatches: 256,
|
|
17
|
+
/** Length of a request id. */
|
|
18
|
+
maxIdLength: 64
|
|
19
|
+
};
|
|
20
|
+
var isId = (value) => typeof value === "string" && value.length > 0 && value.length <= PROTOCOL_LIMITS.maxIdLength;
|
|
21
|
+
var isObjectWithType = (value) => isPlainObject(value) && typeof value.type === "string" && isJsonValue(value);
|
|
22
|
+
/**
|
|
23
|
+
* Reads a message that came from the network. Anything that does not have exactly
|
|
24
|
+
* the expected shape is `undefined`: never trust a client, nor assume a message is one.
|
|
25
|
+
*/
|
|
26
|
+
function parseClientMessage(raw) {
|
|
27
|
+
if (!isPlainObject(raw) || typeof raw.type !== "string") return void 0;
|
|
28
|
+
switch (raw.type) {
|
|
29
|
+
case "sync":
|
|
30
|
+
if (raw.since !== void 0 && !(Number.isInteger(raw.since) && raw.since >= 0)) return void 0;
|
|
31
|
+
return raw.since === void 0 ? { type: "sync" } : {
|
|
32
|
+
type: "sync",
|
|
33
|
+
since: raw.since
|
|
34
|
+
};
|
|
35
|
+
case "intent":
|
|
36
|
+
if (!isId(raw.id) || !Array.isArray(raw.intents)) return void 0;
|
|
37
|
+
if (raw.intents.length === 0 || raw.intents.length > PROTOCOL_LIMITS.maxIntents) return void 0;
|
|
38
|
+
if (!raw.intents.every(isObjectWithType)) return void 0;
|
|
39
|
+
return {
|
|
40
|
+
type: "intent",
|
|
41
|
+
id: raw.id,
|
|
42
|
+
intents: raw.intents
|
|
43
|
+
};
|
|
44
|
+
case "patches":
|
|
45
|
+
if (!isId(raw.id) || !Array.isArray(raw.patches)) return void 0;
|
|
46
|
+
if (raw.patches.length === 0 || raw.patches.length > PROTOCOL_LIMITS.maxPatches) return void 0;
|
|
47
|
+
if (!raw.patches.every(isObjectWithType)) return void 0;
|
|
48
|
+
return {
|
|
49
|
+
type: "patches",
|
|
50
|
+
id: raw.id,
|
|
51
|
+
patches: raw.patches
|
|
52
|
+
};
|
|
53
|
+
default: return;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** Reads a message the authority sent. A client must not crash on garbage either. */
|
|
57
|
+
function parseServerMessage(raw) {
|
|
58
|
+
if (!isPlainObject(raw) || typeof raw.type !== "string") return void 0;
|
|
59
|
+
const revision = raw.revision;
|
|
60
|
+
const hasRevision = Number.isInteger(revision) && revision >= 0;
|
|
61
|
+
switch (raw.type) {
|
|
62
|
+
case "snapshot": return hasRevision && isPlainObject(raw.definition) && isJsonValue(raw.definition) ? {
|
|
63
|
+
type: "snapshot",
|
|
64
|
+
revision,
|
|
65
|
+
definition: raw.definition
|
|
66
|
+
} : void 0;
|
|
67
|
+
case "patches":
|
|
68
|
+
if (!hasRevision || !Array.isArray(raw.patches) || !raw.patches.every(isObjectWithType)) return void 0;
|
|
69
|
+
if (raw.requestId !== void 0 && typeof raw.requestId !== "string") return void 0;
|
|
70
|
+
if (raw.author !== void 0 && typeof raw.author !== "string") return void 0;
|
|
71
|
+
return {
|
|
72
|
+
type: "patches",
|
|
73
|
+
revision,
|
|
74
|
+
patches: raw.patches,
|
|
75
|
+
...raw.requestId !== void 0 ? { requestId: raw.requestId } : {},
|
|
76
|
+
...raw.author !== void 0 ? { author: raw.author } : {}
|
|
77
|
+
};
|
|
78
|
+
case "rejected": return typeof raw.id === "string" && typeof raw.reason === "string" && typeof raw.message === "string" ? {
|
|
79
|
+
type: "rejected",
|
|
80
|
+
id: raw.id,
|
|
81
|
+
reason: raw.reason,
|
|
82
|
+
message: raw.message,
|
|
83
|
+
...Number.isInteger(raw.patchIndex) ? { patchIndex: raw.patchIndex } : {}
|
|
84
|
+
} : void 0;
|
|
85
|
+
case "synced": return hasRevision ? {
|
|
86
|
+
type: "synced",
|
|
87
|
+
revision
|
|
88
|
+
} : void 0;
|
|
89
|
+
case "effect": {
|
|
90
|
+
const target = raw.target;
|
|
91
|
+
if (!(typeof target === "string" || isPlainObject(target) && (target.type === "element" || target.type === "event") && typeof target.id === "string") || typeof raw.name !== "string") return void 0;
|
|
92
|
+
if (raw.params !== void 0 && !(isPlainObject(raw.params) && isJsonValue(raw.params))) return void 0;
|
|
93
|
+
return {
|
|
94
|
+
type: "effect",
|
|
95
|
+
target,
|
|
96
|
+
name: raw.name,
|
|
97
|
+
...raw.params !== void 0 ? { params: raw.params } : {}
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
default: return;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
//#endregion
|
|
104
|
+
export { MAP_BUILDER_EVENT, PROTOCOL_LIMITS, parseClientMessage, parseServerMessage };
|
|
105
|
+
|
|
106
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/protocol/index.ts"],"sourcesContent":["import type { MapDefinition } from \"../model/definition\";\nimport { isJsonValue, isPlainObject } from \"../model/json\";\nimport type { MapPatch } from \"../model/patches\";\nimport type { MapIntent } from \"../interaction/intents\";\n\n/**\n * Messages between a map client and the authority of that map. They are plain JSON, so\n * any transport carries them: a WebSocket, an RPGJS socket event, a Worker message.\n *\n * The client asks (`intent`), the authority decides and tells everybody (`patches`).\n * A client never sends patches unless the authority trusts it.\n */\nexport const MAP_BUILDER_EVENT = \"map-builder\";\n\nexport type ClientMessage =\n /** Join, or catch up. `since` is the last revision the client has. */\n | { readonly type: \"sync\"; readonly since?: number }\n /** Ask for something to happen. `id` comes back in the answer. */\n | { readonly type: \"intent\"; readonly id: string; readonly intents: readonly MapIntent[] }\n /** Ask for patches to be applied as they are. Only for clients the authority trusts (an editor, an undo). */\n | { readonly type: \"patches\"; readonly id: string; readonly patches: readonly MapPatch[] };\n\nexport type ServerMessage =\n /** The whole map, at a revision: to join, or when the client is too far behind. */\n | { readonly type: \"snapshot\"; readonly revision: number; readonly definition: MapDefinition }\n /** A change that became true. `requestId` is set for the client that asked for it. */\n | {\n readonly type: \"patches\";\n readonly revision: number;\n readonly patches: readonly MapPatch[];\n readonly requestId?: string;\n /** Who made the change, when the authority knows and wants to say. */\n readonly author?: string;\n }\n /** The authority said no, and why. */\n | { readonly type: \"rejected\"; readonly id: string; readonly reason: string; readonly message: string; readonly patchIndex?: number }\n /** The client is up to date: nothing to send. */\n | { readonly type: \"synced\"; readonly revision: number }\n /** A short visual effect: it is shown, not kept. */\n | { readonly type: \"effect\"; readonly target: EffectTargetMessage; readonly name: string; readonly params?: Readonly<Record<string, unknown>> };\n\nexport type EffectTargetMessage = string | { readonly type: \"element\" | \"event\"; readonly id: string };\n\n/** What an authority refuses to read, to keep a client from making it work too hard. */\nexport const PROTOCOL_LIMITS = {\n /** Intents in one request. */\n maxIntents: 64,\n /** Patches in one trusted request. */\n maxPatches: 256,\n /** Length of a request id. */\n maxIdLength: 64,\n} as const;\n\nconst isId = (value: unknown): value is string =>\n typeof value === \"string\" && value.length > 0 && value.length <= PROTOCOL_LIMITS.maxIdLength;\n\nconst isObjectWithType = (value: unknown): boolean => isPlainObject(value) && typeof value.type === \"string\" && isJsonValue(value);\n\n/**\n * Reads a message that came from the network. Anything that does not have exactly\n * the expected shape is `undefined`: never trust a client, nor assume a message is one.\n */\nexport function parseClientMessage(raw: unknown): ClientMessage | undefined {\n if (!isPlainObject(raw) || typeof raw.type !== \"string\") return undefined;\n switch (raw.type) {\n case \"sync\":\n if (raw.since !== undefined && !(Number.isInteger(raw.since) && (raw.since as number) >= 0)) return undefined;\n return raw.since === undefined ? { type: \"sync\" } : { type: \"sync\", since: raw.since as number };\n case \"intent\":\n if (!isId(raw.id) || !Array.isArray(raw.intents)) return undefined;\n if (raw.intents.length === 0 || raw.intents.length > PROTOCOL_LIMITS.maxIntents) return undefined;\n if (!raw.intents.every(isObjectWithType)) return undefined;\n return { type: \"intent\", id: raw.id, intents: raw.intents as MapIntent[] };\n case \"patches\":\n if (!isId(raw.id) || !Array.isArray(raw.patches)) return undefined;\n if (raw.patches.length === 0 || raw.patches.length > PROTOCOL_LIMITS.maxPatches) return undefined;\n if (!raw.patches.every(isObjectWithType)) return undefined;\n return { type: \"patches\", id: raw.id, patches: raw.patches as MapPatch[] };\n default:\n return undefined;\n }\n}\n\n/** Reads a message the authority sent. A client must not crash on garbage either. */\nexport function parseServerMessage(raw: unknown): ServerMessage | undefined {\n if (!isPlainObject(raw) || typeof raw.type !== \"string\") return undefined;\n const revision = raw.revision;\n const hasRevision = Number.isInteger(revision) && (revision as number) >= 0;\n switch (raw.type) {\n case \"snapshot\":\n return hasRevision && isPlainObject(raw.definition) && isJsonValue(raw.definition)\n ? { type: \"snapshot\", revision: revision as number, definition: raw.definition as unknown as MapDefinition }\n : undefined;\n case \"patches\":\n if (!hasRevision || !Array.isArray(raw.patches) || !raw.patches.every(isObjectWithType)) return undefined;\n if (raw.requestId !== undefined && typeof raw.requestId !== \"string\") return undefined;\n if (raw.author !== undefined && typeof raw.author !== \"string\") return undefined;\n return {\n type: \"patches\",\n revision: revision as number,\n patches: raw.patches as MapPatch[],\n ...(raw.requestId !== undefined ? { requestId: raw.requestId as string } : {}),\n ...(raw.author !== undefined ? { author: raw.author as string } : {}),\n };\n case \"rejected\":\n return typeof raw.id === \"string\" && typeof raw.reason === \"string\" && typeof raw.message === \"string\"\n ? {\n type: \"rejected\",\n id: raw.id,\n reason: raw.reason,\n message: raw.message,\n ...(Number.isInteger(raw.patchIndex) ? { patchIndex: raw.patchIndex as number } : {}),\n }\n : undefined;\n case \"synced\":\n return hasRevision ? { type: \"synced\", revision: revision as number } : undefined;\n case \"effect\": {\n const target = raw.target;\n const validTarget =\n typeof target === \"string\" ||\n (isPlainObject(target) && (target.type === \"element\" || target.type === \"event\") && typeof target.id === \"string\");\n if (!validTarget || typeof raw.name !== \"string\") return undefined;\n if (raw.params !== undefined && !(isPlainObject(raw.params) && isJsonValue(raw.params))) return undefined;\n return {\n type: \"effect\",\n target: target as EffectTargetMessage,\n name: raw.name,\n ...(raw.params !== undefined ? { params: raw.params as Record<string, unknown> } : {}),\n };\n }\n default:\n return undefined;\n }\n}\n\n/** A way to carry messages, whatever the framework: both ends of a connection implement it. */\nexport interface MapTransport {\n send(message: ClientMessage): void;\n /** Called for every message from the authority. Returns the function that stops listening. */\n onMessage(listener: (message: ServerMessage) => void): () => void;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { MapCommitter } from '../model/committer';
|
|
2
|
+
import { ApplyResult } from '../model/result';
|
|
3
|
+
import { MapTransport, ServerMessage } from '../protocol';
|
|
4
|
+
export interface RemoteCommitterOptions {
|
|
5
|
+
readonly transport: MapTransport;
|
|
6
|
+
/** How long to wait for the authority before giving up on a request. Defaults to 10000. */
|
|
7
|
+
readonly timeoutMs?: number;
|
|
8
|
+
readonly onError?: (error: unknown) => void;
|
|
9
|
+
/** Replaces `setTimeout`, so tests control time. Returns the function that cancels it. */
|
|
10
|
+
readonly schedule?: (run: () => void, delayMs: number) => () => void;
|
|
11
|
+
}
|
|
12
|
+
export interface RemoteEffect {
|
|
13
|
+
readonly target: Extract<ServerMessage, {
|
|
14
|
+
type: "effect";
|
|
15
|
+
}>["target"];
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly params?: Readonly<Record<string, unknown>> | undefined;
|
|
18
|
+
}
|
|
19
|
+
export interface RemoteCommitter extends MapCommitter<Promise<ApplyResult>> {
|
|
20
|
+
/** Called when the authority shows an effect (a shake, a blink) to everybody. */
|
|
21
|
+
onEffect(listener: (effect: RemoteEffect) => void): () => void;
|
|
22
|
+
/**
|
|
23
|
+
* The map the authority sends first. Messages that arrive while the model does not exist
|
|
24
|
+
* yet are kept, and replayed once it is attached.
|
|
25
|
+
*/
|
|
26
|
+
nextSnapshot(timeoutMs?: number): Promise<Extract<ServerMessage, {
|
|
27
|
+
type: "snapshot";
|
|
28
|
+
}>>;
|
|
29
|
+
/** Stops listening. Requests still waiting end with the code `disconnected`. */
|
|
30
|
+
disconnect(): void;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* A committer that does not decide anything: it asks the authority and waits. The change
|
|
34
|
+
* becomes true when the authority says so, for everybody at once, in the same order.
|
|
35
|
+
*
|
|
36
|
+
* - Intents are sent as they are. Patches are only sent when there are no intents (an undo),
|
|
37
|
+
* and the authority refuses them unless it trusts this client.
|
|
38
|
+
* - Changes come back as `patches` messages. Those that answer a request of ours are
|
|
39
|
+
* recorded in the history; the others, made by someone else, are not.
|
|
40
|
+
* - A hole in the revisions, or a change that does not fit, makes it ask to catch up.
|
|
41
|
+
*/
|
|
42
|
+
export declare function remoteCommitter(options: RemoteCommitterOptions): RemoteCommitter;
|
|
43
|
+
//# sourceMappingURL=committer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"committer.d.ts","sourceRoot":"","sources":["../../src/remote/committer.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAA6B,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAClF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,EAA0C,KAAK,YAAY,EAAE,KAAK,aAAa,EAAE,MAAM,aAAa,CAAC;AAE5G,MAAM,WAAW,sBAAsB;IACrC,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,2FAA2F;IAC3F,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;IAC5C,0FAA0F;IAC1F,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,MAAM,IAAI,CAAC;CACtE;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC,aAAa,EAAE;QAAE,IAAI,EAAE,QAAQ,CAAA;KAAE,CAAC,CAAC,QAAQ,CAAC,CAAC;IACtE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC,GAAG,SAAS,CAAC;CACjE;AAED,MAAM,WAAW,eAAgB,SAAQ,YAAY,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;IACzE,iFAAiF;IACjF,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,YAAY,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;IAC/D;;;OAGG;IACH,YAAY,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,aAAa,EAAE;QAAE,IAAI,EAAE,UAAU,CAAA;KAAE,CAAC,CAAC,CAAC;IACxF,gFAAgF;IAChF,UAAU,IAAI,IAAI,CAAC;CACpB;AAoBD;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAAC,OAAO,EAAE,sBAAsB,GAAG,eAAe,CA8KhF"}
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
import { parseServerMessage } from "../protocol/index.js";
|
|
2
|
+
//#region src/remote/committer.ts
|
|
3
|
+
var failure = (code, message, patchIndex) => ({
|
|
4
|
+
ok: false,
|
|
5
|
+
rejection: {
|
|
6
|
+
code,
|
|
7
|
+
message,
|
|
8
|
+
...patchIndex !== void 0 ? { patchIndex } : {}
|
|
9
|
+
}
|
|
10
|
+
});
|
|
11
|
+
var requestCounter = 0;
|
|
12
|
+
var newRequestId = () => {
|
|
13
|
+
return globalThis.crypto?.randomUUID?.() ?? `r${Date.now().toString(36)}-${(requestCounter += 1).toString(36)}`;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* A committer that does not decide anything: it asks the authority and waits. The change
|
|
17
|
+
* becomes true when the authority says so, for everybody at once, in the same order.
|
|
18
|
+
*
|
|
19
|
+
* - Intents are sent as they are. Patches are only sent when there are no intents (an undo),
|
|
20
|
+
* and the authority refuses them unless it trusts this client.
|
|
21
|
+
* - Changes come back as `patches` messages. Those that answer a request of ours are
|
|
22
|
+
* recorded in the history; the others, made by someone else, are not.
|
|
23
|
+
* - A hole in the revisions, or a change that does not fit, makes it ask to catch up.
|
|
24
|
+
*/
|
|
25
|
+
function remoteCommitter(options) {
|
|
26
|
+
const timeoutMs = options.timeoutMs ?? 1e4;
|
|
27
|
+
const reportError = options.onError ?? ((error) => console.error(error));
|
|
28
|
+
const schedule = options.schedule ?? ((run, delayMs) => {
|
|
29
|
+
const handle = setTimeout(run, delayMs);
|
|
30
|
+
return () => clearTimeout(handle);
|
|
31
|
+
});
|
|
32
|
+
let host;
|
|
33
|
+
let disconnected = false;
|
|
34
|
+
let waitingForSync = false;
|
|
35
|
+
let cancelSyncTimeout;
|
|
36
|
+
const queue = [];
|
|
37
|
+
let snapshotWaiter;
|
|
38
|
+
const pending = /* @__PURE__ */ new Map();
|
|
39
|
+
const effectListeners = /* @__PURE__ */ new Set();
|
|
40
|
+
const send = (message) => {
|
|
41
|
+
try {
|
|
42
|
+
options.transport.send(message);
|
|
43
|
+
return true;
|
|
44
|
+
} catch (error) {
|
|
45
|
+
reportError(error);
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
const syncDone = () => {
|
|
50
|
+
waitingForSync = false;
|
|
51
|
+
cancelSyncTimeout?.();
|
|
52
|
+
cancelSyncTimeout = void 0;
|
|
53
|
+
};
|
|
54
|
+
/** Asks the authority to catch us up. Asks again after a while if nothing came back. */
|
|
55
|
+
const requestSync = (full = false) => {
|
|
56
|
+
if (waitingForSync || disconnected) return;
|
|
57
|
+
waitingForSync = true;
|
|
58
|
+
send(full || !host ? { type: "sync" } : {
|
|
59
|
+
type: "sync",
|
|
60
|
+
since: host.revision
|
|
61
|
+
});
|
|
62
|
+
cancelSyncTimeout = schedule(() => {
|
|
63
|
+
waitingForSync = false;
|
|
64
|
+
cancelSyncTimeout = void 0;
|
|
65
|
+
}, timeoutMs);
|
|
66
|
+
};
|
|
67
|
+
const settle = (id, result) => {
|
|
68
|
+
const request = pending.get(id);
|
|
69
|
+
if (!request) return;
|
|
70
|
+
pending.delete(id);
|
|
71
|
+
request.cancelTimeout();
|
|
72
|
+
request.settle(result);
|
|
73
|
+
};
|
|
74
|
+
const handle = (message) => {
|
|
75
|
+
if (disconnected) return;
|
|
76
|
+
switch (message.type) {
|
|
77
|
+
case "effect":
|
|
78
|
+
for (const listener of [...effectListeners]) try {
|
|
79
|
+
listener({
|
|
80
|
+
target: message.target,
|
|
81
|
+
name: message.name,
|
|
82
|
+
params: message.params
|
|
83
|
+
});
|
|
84
|
+
} catch (error) {
|
|
85
|
+
reportError(error);
|
|
86
|
+
}
|
|
87
|
+
return;
|
|
88
|
+
case "rejected":
|
|
89
|
+
settle(message.id, failure(message.reason, message.message, message.patchIndex));
|
|
90
|
+
return;
|
|
91
|
+
case "synced":
|
|
92
|
+
syncDone();
|
|
93
|
+
return;
|
|
94
|
+
case "snapshot":
|
|
95
|
+
if (!host) {
|
|
96
|
+
if (snapshotWaiter) {
|
|
97
|
+
const waiter = snapshotWaiter;
|
|
98
|
+
snapshotWaiter = void 0;
|
|
99
|
+
waiter(message);
|
|
100
|
+
} else queue.push(message);
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
syncDone();
|
|
104
|
+
if (message.revision !== host.revision) host.reset(message.definition, message.revision);
|
|
105
|
+
return;
|
|
106
|
+
case "patches": {
|
|
107
|
+
if (!host) {
|
|
108
|
+
queue.push(message);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
const answered = () => {
|
|
112
|
+
if (message.requestId !== void 0) settle(message.requestId, {
|
|
113
|
+
ok: true,
|
|
114
|
+
revision: message.revision,
|
|
115
|
+
patches: message.patches,
|
|
116
|
+
inverse: []
|
|
117
|
+
});
|
|
118
|
+
};
|
|
119
|
+
if (message.revision <= host.revision) return answered();
|
|
120
|
+
if (message.revision !== host.revision + 1) {
|
|
121
|
+
answered();
|
|
122
|
+
return requestSync();
|
|
123
|
+
}
|
|
124
|
+
const mine = message.requestId !== void 0 ? pending.get(message.requestId) : void 0;
|
|
125
|
+
const result = host.integrate(message.patches, {
|
|
126
|
+
origin: mine ? "local" : "remote",
|
|
127
|
+
revision: message.revision,
|
|
128
|
+
...mine ? {
|
|
129
|
+
record: mine.record,
|
|
130
|
+
...mine.coalesce !== void 0 ? { coalesce: mine.coalesce } : {}
|
|
131
|
+
} : {}
|
|
132
|
+
});
|
|
133
|
+
if (message.requestId !== void 0) settle(message.requestId, result);
|
|
134
|
+
if (!result.ok) requestSync(true);
|
|
135
|
+
else syncDone();
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
const stop = options.transport.onMessage((raw) => {
|
|
141
|
+
const message = parseServerMessage(raw);
|
|
142
|
+
if (message) handle(message);
|
|
143
|
+
});
|
|
144
|
+
return {
|
|
145
|
+
attach(next) {
|
|
146
|
+
if (host && host !== next) throw new Error("A committer can only be attached to one model");
|
|
147
|
+
host = next;
|
|
148
|
+
for (const message of queue.splice(0)) handle(message);
|
|
149
|
+
},
|
|
150
|
+
nextSnapshot(waitMs = timeoutMs) {
|
|
151
|
+
return new Promise((resolve, reject) => {
|
|
152
|
+
const index = queue.findIndex((message) => message.type === "snapshot");
|
|
153
|
+
if (index >= 0) return resolve(queue.splice(index, 1)[0]);
|
|
154
|
+
const cancel = schedule(() => {
|
|
155
|
+
snapshotWaiter = void 0;
|
|
156
|
+
reject(/* @__PURE__ */ new Error("The authority did not send the map"));
|
|
157
|
+
}, waitMs);
|
|
158
|
+
snapshotWaiter = (message) => {
|
|
159
|
+
cancel();
|
|
160
|
+
resolve(message);
|
|
161
|
+
};
|
|
162
|
+
});
|
|
163
|
+
},
|
|
164
|
+
commit(request) {
|
|
165
|
+
if (disconnected) return Promise.resolve(failure("disconnected", "The connection to the authority is closed"));
|
|
166
|
+
const id = newRequestId();
|
|
167
|
+
const message = request.intents && request.intents.length > 0 ? {
|
|
168
|
+
type: "intent",
|
|
169
|
+
id,
|
|
170
|
+
intents: request.intents
|
|
171
|
+
} : {
|
|
172
|
+
type: "patches",
|
|
173
|
+
id,
|
|
174
|
+
patches: request.patches
|
|
175
|
+
};
|
|
176
|
+
return new Promise((resolve) => {
|
|
177
|
+
const cancelTimeout = schedule(() => {
|
|
178
|
+
if (pending.delete(id)) resolve(failure("timeout", "The authority did not answer"));
|
|
179
|
+
}, timeoutMs);
|
|
180
|
+
pending.set(id, {
|
|
181
|
+
record: request.record,
|
|
182
|
+
coalesce: request.coalesce,
|
|
183
|
+
settle: resolve,
|
|
184
|
+
cancelTimeout
|
|
185
|
+
});
|
|
186
|
+
if (!send(message)) settle(id, failure("transport-error", "The message could not be sent"));
|
|
187
|
+
});
|
|
188
|
+
},
|
|
189
|
+
onEffect(listener) {
|
|
190
|
+
effectListeners.add(listener);
|
|
191
|
+
return () => effectListeners.delete(listener);
|
|
192
|
+
},
|
|
193
|
+
disconnect() {
|
|
194
|
+
if (disconnected) return;
|
|
195
|
+
disconnected = true;
|
|
196
|
+
stop();
|
|
197
|
+
syncDone();
|
|
198
|
+
for (const id of [...pending.keys()]) settle(id, failure("disconnected", "The connection to the authority is closed"));
|
|
199
|
+
effectListeners.clear();
|
|
200
|
+
}
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
//#endregion
|
|
204
|
+
export { remoteCommitter };
|
|
205
|
+
|
|
206
|
+
//# sourceMappingURL=committer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"committer.js","names":[],"sources":["../../src/remote/committer.ts"],"sourcesContent":["import type { CommitHost, CommitRequest, MapCommitter } from \"../model/committer\";\nimport type { ApplyResult } from \"../model/result\";\nimport { parseServerMessage, type ClientMessage, type MapTransport, type ServerMessage } from \"../protocol\";\n\nexport interface RemoteCommitterOptions {\n readonly transport: MapTransport;\n /** How long to wait for the authority before giving up on a request. Defaults to 10000. */\n readonly timeoutMs?: number;\n readonly onError?: (error: unknown) => void;\n /** Replaces `setTimeout`, so tests control time. Returns the function that cancels it. */\n readonly schedule?: (run: () => void, delayMs: number) => () => void;\n}\n\nexport interface RemoteEffect {\n readonly target: Extract<ServerMessage, { type: \"effect\" }>[\"target\"];\n readonly name: string;\n readonly params?: Readonly<Record<string, unknown>> | undefined;\n}\n\nexport interface RemoteCommitter extends MapCommitter<Promise<ApplyResult>> {\n /** Called when the authority shows an effect (a shake, a blink) to everybody. */\n onEffect(listener: (effect: RemoteEffect) => void): () => void;\n /**\n * The map the authority sends first. Messages that arrive while the model does not exist\n * yet are kept, and replayed once it is attached.\n */\n nextSnapshot(timeoutMs?: number): Promise<Extract<ServerMessage, { type: \"snapshot\" }>>;\n /** Stops listening. Requests still waiting end with the code `disconnected`. */\n disconnect(): void;\n}\n\ninterface Pending {\n readonly record: boolean;\n readonly coalesce: string | undefined;\n readonly settle: (result: ApplyResult) => void;\n readonly cancelTimeout: () => void;\n}\n\nconst failure = (code: string, message: string, patchIndex?: number): ApplyResult => ({\n ok: false,\n rejection: { code, message, ...(patchIndex !== undefined ? { patchIndex } : {}) },\n});\n\nlet requestCounter = 0;\nconst newRequestId = (): string => {\n const random = globalThis.crypto?.randomUUID?.();\n return random ?? `r${Date.now().toString(36)}-${(requestCounter += 1).toString(36)}`;\n};\n\n/**\n * A committer that does not decide anything: it asks the authority and waits. The change\n * becomes true when the authority says so, for everybody at once, in the same order.\n *\n * - Intents are sent as they are. Patches are only sent when there are no intents (an undo),\n * and the authority refuses them unless it trusts this client.\n * - Changes come back as `patches` messages. Those that answer a request of ours are\n * recorded in the history; the others, made by someone else, are not.\n * - A hole in the revisions, or a change that does not fit, makes it ask to catch up.\n */\nexport function remoteCommitter(options: RemoteCommitterOptions): RemoteCommitter {\n const timeoutMs = options.timeoutMs ?? 10_000;\n const reportError = options.onError ?? ((error: unknown) => console.error(error));\n const schedule =\n options.schedule ??\n ((run: () => void, delayMs: number) => {\n const handle = setTimeout(run, delayMs);\n return () => clearTimeout(handle);\n });\n\n let host: CommitHost | undefined;\n let disconnected = false;\n let waitingForSync = false;\n let cancelSyncTimeout: (() => void) | undefined;\n const queue: ServerMessage[] = [];\n let snapshotWaiter: ((message: Extract<ServerMessage, { type: \"snapshot\" }>) => void) | undefined;\n const pending = new Map<string, Pending>();\n const effectListeners = new Set<(effect: RemoteEffect) => void>();\n\n const send = (message: ClientMessage): boolean => {\n try {\n options.transport.send(message);\n return true;\n } catch (error) {\n reportError(error);\n return false;\n }\n };\n\n const syncDone = (): void => {\n waitingForSync = false;\n cancelSyncTimeout?.();\n cancelSyncTimeout = undefined;\n };\n\n /** Asks the authority to catch us up. Asks again after a while if nothing came back. */\n const requestSync = (full = false): void => {\n if (waitingForSync || disconnected) return;\n waitingForSync = true;\n send(full || !host ? { type: \"sync\" } : { type: \"sync\", since: host.revision });\n cancelSyncTimeout = schedule(() => {\n waitingForSync = false;\n cancelSyncTimeout = undefined;\n }, timeoutMs);\n };\n\n const settle = (id: string, result: ApplyResult): void => {\n const request = pending.get(id);\n if (!request) return;\n pending.delete(id);\n request.cancelTimeout();\n request.settle(result);\n };\n\n const handle = (message: ServerMessage): void => {\n if (disconnected) return;\n switch (message.type) {\n case \"effect\":\n for (const listener of [...effectListeners]) {\n try {\n listener({ target: message.target, name: message.name, params: message.params });\n } catch (error) {\n reportError(error);\n }\n }\n return;\n case \"rejected\":\n settle(message.id, failure(message.reason, message.message, message.patchIndex));\n return;\n case \"synced\":\n syncDone();\n return;\n case \"snapshot\":\n if (!host) {\n if (snapshotWaiter) {\n const waiter = snapshotWaiter;\n snapshotWaiter = undefined;\n waiter(message);\n } else queue.push(message);\n return;\n }\n syncDone();\n // The same revision is the same map: no need to rebuild everything for a repeated snapshot.\n if (message.revision !== host.revision) host.reset(message.definition, message.revision);\n return;\n case \"patches\": {\n if (!host) {\n queue.push(message);\n return;\n }\n // The authority applied one of our requests: it is done even if we cannot integrate it here\n // (we caught up past it, or we are catching up), so the caller must not wait for a timeout.\n const answered = (): void => {\n if (message.requestId !== undefined) settle(message.requestId, { ok: true, revision: message.revision, patches: message.patches, inverse: [] });\n };\n // Already known: a repeat, or a change we got while catching up.\n if (message.revision <= host.revision) return answered();\n if (message.revision !== host.revision + 1) {\n answered();\n return requestSync();\n }\n\n const mine = message.requestId !== undefined ? pending.get(message.requestId) : undefined;\n const result = host.integrate(message.patches, {\n origin: mine ? \"local\" : \"remote\",\n revision: message.revision,\n ...(mine ? { record: mine.record, ...(mine.coalesce !== undefined ? { coalesce: mine.coalesce } : {}) } : {}),\n });\n if (message.requestId !== undefined) settle(message.requestId, result);\n // A change that does not fit means we drifted: start again from the map.\n if (!result.ok) requestSync(true);\n else syncDone();\n return;\n }\n }\n };\n\n // Listening starts now, not when the model exists: nothing the authority says may be lost.\n // Whatever comes from the wire is checked first: a client never acts on garbage.\n const stop = options.transport.onMessage((raw) => {\n const message = parseServerMessage(raw);\n if (message) handle(message);\n });\n\n return {\n attach(next) {\n if (host && host !== next) throw new Error(\"A committer can only be attached to one model\");\n host = next;\n // What arrived before the model existed, in order.\n for (const message of queue.splice(0)) handle(message);\n },\n nextSnapshot(waitMs = timeoutMs) {\n return new Promise((resolve, reject) => {\n const index = queue.findIndex((message) => message.type === \"snapshot\");\n if (index >= 0) return resolve(queue.splice(index, 1)[0] as never);\n const cancel = schedule(() => {\n snapshotWaiter = undefined;\n reject(new Error(\"The authority did not send the map\"));\n }, waitMs);\n snapshotWaiter = (message) => {\n cancel();\n resolve(message);\n };\n });\n },\n commit(request: CommitRequest) {\n if (disconnected) return Promise.resolve(failure(\"disconnected\", \"The connection to the authority is closed\"));\n const id = newRequestId();\n const message: ClientMessage =\n request.intents && request.intents.length > 0\n ? { type: \"intent\", id, intents: request.intents as never }\n : { type: \"patches\", id, patches: request.patches };\n\n return new Promise<ApplyResult>((resolve) => {\n const cancelTimeout = schedule(() => {\n if (pending.delete(id)) resolve(failure(\"timeout\", \"The authority did not answer\"));\n }, timeoutMs);\n pending.set(id, { record: request.record, coalesce: request.coalesce, settle: resolve, cancelTimeout });\n if (!send(message)) settle(id, failure(\"transport-error\", \"The message could not be sent\"));\n });\n },\n onEffect(listener) {\n effectListeners.add(listener);\n return () => effectListeners.delete(listener);\n },\n disconnect() {\n if (disconnected) return;\n disconnected = true;\n stop();\n syncDone();\n for (const id of [...pending.keys()]) settle(id, failure(\"disconnected\", \"The connection to the authority is closed\"));\n effectListeners.clear();\n },\n };\n}\n"],"mappings":";;AAsCA,IAAM,WAAW,MAAc,SAAiB,gBAAsC;CACpF,IAAI;CACJ,WAAW;EAAE;EAAM;EAAS,GAAI,eAAe,KAAA,IAAY,EAAE,WAAW,IAAI,CAAC;CAAG;AAClF;AAEA,IAAI,iBAAiB;AACrB,IAAM,qBAA6B;CAEjC,OADe,WAAW,QAAQ,aAAa,KAC9B,IAAI,KAAK,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,IAAI,kBAAkB,EAAA,CAAG,SAAS,EAAE;AACnF;;;;;;;;;;;AAYA,SAAgB,gBAAgB,SAAkD;CAChF,MAAM,YAAY,QAAQ,aAAa;CACvC,MAAM,cAAc,QAAQ,aAAa,UAAmB,QAAQ,MAAM,KAAK;CAC/E,MAAM,WACJ,QAAQ,cACN,KAAiB,YAAoB;EACrC,MAAM,SAAS,WAAW,KAAK,OAAO;EACtC,aAAa,aAAa,MAAM;CAClC;CAEF,IAAI;CACJ,IAAI,eAAe;CACnB,IAAI,iBAAiB;CACrB,IAAI;CACJ,MAAM,QAAyB,CAAC;CAChC,IAAI;CACJ,MAAM,0BAAU,IAAI,IAAqB;CACzC,MAAM,kCAAkB,IAAI,IAAoC;CAEhE,MAAM,QAAQ,YAAoC;EAChD,IAAI;GACF,QAAQ,UAAU,KAAK,OAAO;GAC9B,OAAO;EACT,SAAS,OAAO;GACd,YAAY,KAAK;GACjB,OAAO;EACT;CACF;CAEA,MAAM,iBAAuB;EAC3B,iBAAiB;EACjB,oBAAoB;EACpB,oBAAoB,KAAA;CACtB;;CAGA,MAAM,eAAe,OAAO,UAAgB;EAC1C,IAAI,kBAAkB,cAAc;EACpC,iBAAiB;EACjB,KAAK,QAAQ,CAAC,OAAO,EAAE,MAAM,OAAO,IAAI;GAAE,MAAM;GAAQ,OAAO,KAAK;EAAS,CAAC;EAC9E,oBAAoB,eAAe;GACjC,iBAAiB;GACjB,oBAAoB,KAAA;EACtB,GAAG,SAAS;CACd;CAEA,MAAM,UAAU,IAAY,WAA8B;EACxD,MAAM,UAAU,QAAQ,IAAI,EAAE;EAC9B,IAAI,CAAC,SAAS;EACd,QAAQ,OAAO,EAAE;EACjB,QAAQ,cAAc;EACtB,QAAQ,OAAO,MAAM;CACvB;CAEA,MAAM,UAAU,YAAiC;EAC/C,IAAI,cAAc;EAClB,QAAQ,QAAQ,MAAhB;GACE,KAAK;IACH,KAAK,MAAM,YAAY,CAAC,GAAG,eAAe,GACxC,IAAI;KACF,SAAS;MAAE,QAAQ,QAAQ;MAAQ,MAAM,QAAQ;MAAM,QAAQ,QAAQ;KAAO,CAAC;IACjF,SAAS,OAAO;KACd,YAAY,KAAK;IACnB;IAEF;GACF,KAAK;IACH,OAAO,QAAQ,IAAI,QAAQ,QAAQ,QAAQ,QAAQ,SAAS,QAAQ,UAAU,CAAC;IAC/E;GACF,KAAK;IACH,SAAS;IACT;GACF,KAAK;IACH,IAAI,CAAC,MAAM;KACT,IAAI,gBAAgB;MAClB,MAAM,SAAS;MACf,iBAAiB,KAAA;MACjB,OAAO,OAAO;KAChB,OAAO,MAAM,KAAK,OAAO;KACzB;IACF;IACA,SAAS;IAET,IAAI,QAAQ,aAAa,KAAK,UAAU,KAAK,MAAM,QAAQ,YAAY,QAAQ,QAAQ;IACvF;GACF,KAAK,WAAW;IACd,IAAI,CAAC,MAAM;KACT,MAAM,KAAK,OAAO;KAClB;IACF;IAGA,MAAM,iBAAuB;KAC3B,IAAI,QAAQ,cAAc,KAAA,GAAW,OAAO,QAAQ,WAAW;MAAE,IAAI;MAAM,UAAU,QAAQ;MAAU,SAAS,QAAQ;MAAS,SAAS,CAAC;KAAE,CAAC;IAChJ;IAEA,IAAI,QAAQ,YAAY,KAAK,UAAU,OAAO,SAAS;IACvD,IAAI,QAAQ,aAAa,KAAK,WAAW,GAAG;KAC1C,SAAS;KACT,OAAO,YAAY;IACrB;IAEA,MAAM,OAAO,QAAQ,cAAc,KAAA,IAAY,QAAQ,IAAI,QAAQ,SAAS,IAAI,KAAA;IAChF,MAAM,SAAS,KAAK,UAAU,QAAQ,SAAS;KAC7C,QAAQ,OAAO,UAAU;KACzB,UAAU,QAAQ;KAClB,GAAI,OAAO;MAAE,QAAQ,KAAK;MAAQ,GAAI,KAAK,aAAa,KAAA,IAAY,EAAE,UAAU,KAAK,SAAS,IAAI,CAAC;KAAG,IAAI,CAAC;IAC7G,CAAC;IACD,IAAI,QAAQ,cAAc,KAAA,GAAW,OAAO,QAAQ,WAAW,MAAM;IAErE,IAAI,CAAC,OAAO,IAAI,YAAY,IAAI;SAC3B,SAAS;IACd;GACF;EACF;CACF;CAIA,MAAM,OAAO,QAAQ,UAAU,WAAW,QAAQ;EAChD,MAAM,UAAU,mBAAmB,GAAG;EACtC,IAAI,SAAS,OAAO,OAAO;CAC7B,CAAC;CAED,OAAO;EACL,OAAO,MAAM;GACX,IAAI,QAAQ,SAAS,MAAM,MAAM,IAAI,MAAM,+CAA+C;GAC1F,OAAO;GAEP,KAAK,MAAM,WAAW,MAAM,OAAO,CAAC,GAAG,OAAO,OAAO;EACvD;EACA,aAAa,SAAS,WAAW;GAC/B,OAAO,IAAI,SAAS,SAAS,WAAW;IACtC,MAAM,QAAQ,MAAM,WAAW,YAAY,QAAQ,SAAS,UAAU;IACtE,IAAI,SAAS,GAAG,OAAO,QAAQ,MAAM,OAAO,OAAO,CAAC,CAAC,CAAC,EAAW;IACjE,MAAM,SAAS,eAAe;KAC5B,iBAAiB,KAAA;KACjB,uBAAO,IAAI,MAAM,oCAAoC,CAAC;IACxD,GAAG,MAAM;IACT,kBAAkB,YAAY;KAC5B,OAAO;KACP,QAAQ,OAAO;IACjB;GACF,CAAC;EACH;EACA,OAAO,SAAwB;GAC7B,IAAI,cAAc,OAAO,QAAQ,QAAQ,QAAQ,gBAAgB,2CAA2C,CAAC;GAC7G,MAAM,KAAK,aAAa;GACxB,MAAM,UACJ,QAAQ,WAAW,QAAQ,QAAQ,SAAS,IACxC;IAAE,MAAM;IAAU;IAAI,SAAS,QAAQ;GAAiB,IACxD;IAAE,MAAM;IAAW;IAAI,SAAS,QAAQ;GAAQ;GAEtD,OAAO,IAAI,SAAsB,YAAY;IAC3C,MAAM,gBAAgB,eAAe;KACnC,IAAI,QAAQ,OAAO,EAAE,GAAG,QAAQ,QAAQ,WAAW,8BAA8B,CAAC;IACpF,GAAG,SAAS;IACZ,QAAQ,IAAI,IAAI;KAAE,QAAQ,QAAQ;KAAQ,UAAU,QAAQ;KAAU,QAAQ;KAAS;IAAc,CAAC;IACtG,IAAI,CAAC,KAAK,OAAO,GAAG,OAAO,IAAI,QAAQ,mBAAmB,+BAA+B,CAAC;GAC5F,CAAC;EACH;EACA,SAAS,UAAU;GACjB,gBAAgB,IAAI,QAAQ;GAC5B,aAAa,gBAAgB,OAAO,QAAQ;EAC9C;EACA,aAAa;GACX,IAAI,cAAc;GAClB,eAAe;GACf,KAAK;GACL,SAAS;GACT,KAAK,MAAM,MAAM,CAAC,GAAG,QAAQ,KAAK,CAAC,GAAG,OAAO,IAAI,QAAQ,gBAAgB,2CAA2C,CAAC;GACrH,gBAAgB,MAAM;EACxB;CACF;AACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/remote/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,QAAQ,CAAC;AACvB,cAAc,oBAAoB,CAAC"}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { MapModel, MapModelOptions } from '../model/model';
|
|
2
|
+
import { ApplyResult } from '../model/result';
|
|
3
|
+
import { MapTransport } from '../protocol';
|
|
4
|
+
import { RemoteCommitter, RemoteCommitterOptions, RemoteEffect } from './committer';
|
|
5
|
+
export interface JoinRemoteMapOptions extends Omit<RemoteCommitterOptions, "transport"> {
|
|
6
|
+
readonly transport: MapTransport;
|
|
7
|
+
/** Options of the model, other than the committer and the revision, which come from the authority. */
|
|
8
|
+
readonly model?: Omit<MapModelOptions, "committer" | "revision">;
|
|
9
|
+
}
|
|
10
|
+
export interface RemoteMap {
|
|
11
|
+
/** The map as the authority has it. Changes go through the authority: `apply` answers when it has decided. */
|
|
12
|
+
readonly model: MapModel<Promise<ApplyResult>>;
|
|
13
|
+
readonly committer: RemoteCommitter;
|
|
14
|
+
/** Stops listening to the authority. */
|
|
15
|
+
disconnect(): void;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Joins the map of an authority: asks for it, waits for it, and returns a model that
|
|
19
|
+
* stays in step with everybody else's changes.
|
|
20
|
+
*/
|
|
21
|
+
export declare function joinRemoteMap(options: JoinRemoteMapOptions): Promise<RemoteMap>;
|
|
22
|
+
/** Anything that can play an effect on an element or an event: a view's `effects`. */
|
|
23
|
+
export interface EffectPlayer {
|
|
24
|
+
play(target: RemoteEffect["target"], name: string, params?: Record<string, unknown>): unknown;
|
|
25
|
+
}
|
|
26
|
+
/** Shows the effects the authority plays: `bindRemoteEffects(remote, view.effects)`. Returns the function that stops. */
|
|
27
|
+
export declare function bindRemoteEffects(remote: {
|
|
28
|
+
readonly committer: RemoteCommitter;
|
|
29
|
+
}, effects: EffectPlayer): () => void;
|
|
30
|
+
//# sourceMappingURL=join.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"join.d.ts","sourceRoot":"","sources":["../../src/remote/join.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,QAAQ,EAAE,KAAK,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACrF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AACnD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAChD,OAAO,EAAmB,KAAK,eAAe,EAAE,KAAK,sBAAsB,EAAE,KAAK,YAAY,EAAE,MAAM,aAAa,CAAC;AAEpH,MAAM,WAAW,oBAAqB,SAAQ,IAAI,CAAC,sBAAsB,EAAE,WAAW,CAAC;IACrF,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,sGAAsG;IACtG,QAAQ,CAAC,KAAK,CAAC,EAAE,IAAI,CAAC,eAAe,EAAE,WAAW,GAAG,UAAU,CAAC,CAAC;CAClE;AAED,MAAM,WAAW,SAAS;IACxB,8GAA8G;IAC9G,QAAQ,CAAC,KAAK,EAAE,QAAQ,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC,CAAC;IAC/C,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAC;IACpC,wCAAwC;IACxC,UAAU,IAAI,IAAI,CAAC;CACpB;AAED;;;GAGG;AACH,wBAAsB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,SAAS,CAAC,CAgBrF;AAED,sFAAsF;AACtF,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,OAAO,CAAC;CAC/F;AAED,yHAAyH;AACzH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE;IAAE,QAAQ,CAAC,SAAS,EAAE,eAAe,CAAA;CAAE,EAAE,OAAO,EAAE,YAAY,GAAG,MAAM,IAAI,CAEpH"}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { createMapModel } from "../model/model.js";
|
|
2
|
+
import { remoteCommitter } from "./committer.js";
|
|
3
|
+
//#region src/remote/join.ts
|
|
4
|
+
/**
|
|
5
|
+
* Joins the map of an authority: asks for it, waits for it, and returns a model that
|
|
6
|
+
* stays in step with everybody else's changes.
|
|
7
|
+
*/
|
|
8
|
+
async function joinRemoteMap(options) {
|
|
9
|
+
const committer = remoteCommitter(options);
|
|
10
|
+
const waiting = committer.nextSnapshot();
|
|
11
|
+
options.transport.send({ type: "sync" });
|
|
12
|
+
let first;
|
|
13
|
+
try {
|
|
14
|
+
first = await waiting;
|
|
15
|
+
} catch (error) {
|
|
16
|
+
committer.disconnect();
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
return {
|
|
20
|
+
model: createMapModel(first.definition, {
|
|
21
|
+
...options.model,
|
|
22
|
+
committer,
|
|
23
|
+
revision: first.revision
|
|
24
|
+
}),
|
|
25
|
+
committer,
|
|
26
|
+
disconnect: () => committer.disconnect()
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
/** Shows the effects the authority plays: `bindRemoteEffects(remote, view.effects)`. Returns the function that stops. */
|
|
30
|
+
function bindRemoteEffects(remote, effects) {
|
|
31
|
+
return remote.committer.onEffect((effect) => void effects.play(effect.target, effect.name, effect.params));
|
|
32
|
+
}
|
|
33
|
+
//#endregion
|
|
34
|
+
export { bindRemoteEffects, joinRemoteMap };
|
|
35
|
+
|
|
36
|
+
//# sourceMappingURL=join.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"join.js","names":[],"sources":["../../src/remote/join.ts"],"sourcesContent":["import { createMapModel, type MapModel, type MapModelOptions } from \"../model/model\";\nimport type { ApplyResult } from \"../model/result\";\nimport type { MapTransport } from \"../protocol\";\nimport { remoteCommitter, type RemoteCommitter, type RemoteCommitterOptions, type RemoteEffect } from \"./committer\";\n\nexport interface JoinRemoteMapOptions extends Omit<RemoteCommitterOptions, \"transport\"> {\n readonly transport: MapTransport;\n /** Options of the model, other than the committer and the revision, which come from the authority. */\n readonly model?: Omit<MapModelOptions, \"committer\" | \"revision\">;\n}\n\nexport interface RemoteMap {\n /** The map as the authority has it. Changes go through the authority: `apply` answers when it has decided. */\n readonly model: MapModel<Promise<ApplyResult>>;\n readonly committer: RemoteCommitter;\n /** Stops listening to the authority. */\n disconnect(): void;\n}\n\n/**\n * Joins the map of an authority: asks for it, waits for it, and returns a model that\n * stays in step with everybody else's changes.\n */\nexport async function joinRemoteMap(options: JoinRemoteMapOptions): Promise<RemoteMap> {\n const committer = remoteCommitter(options);\n // Ask after listening: the authority may also send the map on its own, on connection.\n const waiting = committer.nextSnapshot();\n options.transport.send({ type: \"sync\" });\n\n let first;\n try {\n first = await waiting;\n } catch (error) {\n committer.disconnect();\n throw error;\n }\n\n const model = createMapModel(first.definition, { ...options.model, committer, revision: first.revision });\n return { model, committer, disconnect: () => committer.disconnect() };\n}\n\n/** Anything that can play an effect on an element or an event: a view's `effects`. */\nexport interface EffectPlayer {\n play(target: RemoteEffect[\"target\"], name: string, params?: Record<string, unknown>): unknown;\n}\n\n/** Shows the effects the authority plays: `bindRemoteEffects(remote, view.effects)`. Returns the function that stops. */\nexport function bindRemoteEffects(remote: { readonly committer: RemoteCommitter }, effects: EffectPlayer): () => void {\n return remote.committer.onEffect((effect) => void effects.play(effect.target, effect.name, effect.params as Record<string, unknown> | undefined));\n}\n"],"mappings":";;;;;;;AAuBA,eAAsB,cAAc,SAAmD;CACrF,MAAM,YAAY,gBAAgB,OAAO;CAEzC,MAAM,UAAU,UAAU,aAAa;CACvC,QAAQ,UAAU,KAAK,EAAE,MAAM,OAAO,CAAC;CAEvC,IAAI;CACJ,IAAI;EACF,QAAQ,MAAM;CAChB,SAAS,OAAO;EACd,UAAU,WAAW;EACrB,MAAM;CACR;CAGA,OAAO;EAAE,OADK,eAAe,MAAM,YAAY;GAAE,GAAG,QAAQ;GAAO;GAAW,UAAU,MAAM;EAAS,CAC9F;EAAO;EAAW,kBAAkB,UAAU,WAAW;CAAE;AACtE;;AAQA,SAAgB,kBAAkB,QAAiD,SAAmC;CACpH,OAAO,OAAO,UAAU,UAAU,WAAW,KAAK,QAAQ,KAAK,OAAO,QAAQ,OAAO,MAAM,OAAO,MAA6C,CAAC;AAClJ"}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { AuthoritySession, MapAuthority } from '../authority/authority';
|
|
2
|
+
import { MapTransport } from '../protocol';
|
|
3
|
+
export interface MemoryConnectionOptions {
|
|
4
|
+
/** Who is acting for this connection. Handed to the pipelines. */
|
|
5
|
+
readonly actor?: unknown;
|
|
6
|
+
readonly id?: string;
|
|
7
|
+
/** `"microtask"` (default) delivers like a network would, after the current code; `"sync"` at once. */
|
|
8
|
+
readonly delivery?: "microtask" | "sync";
|
|
9
|
+
}
|
|
10
|
+
export interface MemoryConnection {
|
|
11
|
+
readonly transport: MapTransport;
|
|
12
|
+
readonly session: AuthoritySession;
|
|
13
|
+
disconnect(): void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Connects a client to an authority in the same process, with no network: for tests, for
|
|
17
|
+
* a demo, for a single-player game that still wants the rules of the authority.
|
|
18
|
+
*/
|
|
19
|
+
export declare function connectInMemory(authority: MapAuthority, options?: MemoryConnectionOptions): MemoryConnection;
|
|
20
|
+
//# sourceMappingURL=memory-transport.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-transport.d.ts","sourceRoot":"","sources":["../../src/remote/memory-transport.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAE,YAAY,EAAE,MAAM,wBAAwB,CAAC;AAC7E,OAAO,KAAK,EAAiB,YAAY,EAAiB,MAAM,aAAa,CAAC;AAE9E,MAAM,WAAW,uBAAuB;IACtC,kEAAkE;IAClE,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAC;IACrB,uGAAuG;IACvG,QAAQ,CAAC,QAAQ,CAAC,EAAE,WAAW,GAAG,MAAM,CAAC;CAC1C;AAED,MAAM,WAAW,gBAAgB;IAC/B,QAAQ,CAAC,SAAS,EAAE,YAAY,CAAC;IACjC,QAAQ,CAAC,OAAO,EAAE,gBAAgB,CAAC;IACnC,UAAU,IAAI,IAAI,CAAC;CACpB;AAID;;;GAGG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,YAAY,EAAE,OAAO,GAAE,uBAA4B,GAAG,gBAAgB,CAwChH"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
//#region src/remote/memory-transport.ts
|
|
2
|
+
var sessionCounter = 0;
|
|
3
|
+
/**
|
|
4
|
+
* Connects a client to an authority in the same process, with no network: for tests, for
|
|
5
|
+
* a demo, for a single-player game that still wants the rules of the authority.
|
|
6
|
+
*/
|
|
7
|
+
function connectInMemory(authority, options = {}) {
|
|
8
|
+
const listeners = /* @__PURE__ */ new Set();
|
|
9
|
+
const deliver = options.delivery ?? "microtask";
|
|
10
|
+
const later = (run) => deliver === "sync" ? run() : queueMicrotask(run);
|
|
11
|
+
let open = true;
|
|
12
|
+
const session = {
|
|
13
|
+
id: options.id ?? `memory-${sessionCounter += 1}`,
|
|
14
|
+
actor: options.actor,
|
|
15
|
+
send(message) {
|
|
16
|
+
const copy = JSON.parse(JSON.stringify(message));
|
|
17
|
+
later(() => {
|
|
18
|
+
if (open) for (const listener of [...listeners]) listener(copy);
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
const connection = authority.connect(session);
|
|
23
|
+
return {
|
|
24
|
+
session,
|
|
25
|
+
transport: {
|
|
26
|
+
send(message) {
|
|
27
|
+
if (!open) return;
|
|
28
|
+
const copy = JSON.parse(JSON.stringify(message));
|
|
29
|
+
later(() => {
|
|
30
|
+
if (open) connection.receive(copy);
|
|
31
|
+
});
|
|
32
|
+
},
|
|
33
|
+
onMessage(listener) {
|
|
34
|
+
listeners.add(listener);
|
|
35
|
+
return () => listeners.delete(listener);
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
disconnect() {
|
|
39
|
+
open = false;
|
|
40
|
+
connection.disconnect();
|
|
41
|
+
listeners.clear();
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
//#endregion
|
|
46
|
+
export { connectInMemory };
|
|
47
|
+
|
|
48
|
+
//# sourceMappingURL=memory-transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"memory-transport.js","names":[],"sources":["../../src/remote/memory-transport.ts"],"sourcesContent":["import type { AuthoritySession, MapAuthority } from \"../authority/authority\";\nimport type { ClientMessage, MapTransport, ServerMessage } from \"../protocol\";\n\nexport interface MemoryConnectionOptions {\n /** Who is acting for this connection. Handed to the pipelines. */\n readonly actor?: unknown;\n readonly id?: string;\n /** `\"microtask\"` (default) delivers like a network would, after the current code; `\"sync\"` at once. */\n readonly delivery?: \"microtask\" | \"sync\";\n}\n\nexport interface MemoryConnection {\n readonly transport: MapTransport;\n readonly session: AuthoritySession;\n disconnect(): void;\n}\n\nlet sessionCounter = 0;\n\n/**\n * Connects a client to an authority in the same process, with no network: for tests, for\n * a demo, for a single-player game that still wants the rules of the authority.\n */\nexport function connectInMemory(authority: MapAuthority, options: MemoryConnectionOptions = {}): MemoryConnection {\n const listeners = new Set<(message: ServerMessage) => void>();\n const deliver = options.delivery ?? \"microtask\";\n const later = (run: () => void): void => (deliver === \"sync\" ? run() : queueMicrotask(run));\n let open = true;\n\n const session: AuthoritySession = {\n id: options.id ?? `memory-${(sessionCounter += 1)}`,\n actor: options.actor,\n send(message) {\n // Through JSON, as a network would: nothing is shared between the two sides.\n const copy = JSON.parse(JSON.stringify(message)) as ServerMessage;\n later(() => {\n if (open) for (const listener of [...listeners]) listener(copy);\n });\n },\n };\n\n const connection = authority.connect(session);\n return {\n session,\n transport: {\n send(message: ClientMessage) {\n if (!open) return;\n const copy = JSON.parse(JSON.stringify(message)) as unknown;\n later(() => {\n if (open) connection.receive(copy);\n });\n },\n onMessage(listener) {\n listeners.add(listener);\n return () => listeners.delete(listener);\n },\n },\n disconnect() {\n open = false;\n connection.disconnect();\n listeners.clear();\n },\n };\n}\n"],"mappings":";AAiBA,IAAI,iBAAiB;;;;;AAMrB,SAAgB,gBAAgB,WAAyB,UAAmC,CAAC,GAAqB;CAChH,MAAM,4BAAY,IAAI,IAAsC;CAC5D,MAAM,UAAU,QAAQ,YAAY;CACpC,MAAM,SAAS,QAA2B,YAAY,SAAS,IAAI,IAAI,eAAe,GAAG;CACzF,IAAI,OAAO;CAEX,MAAM,UAA4B;EAChC,IAAI,QAAQ,MAAM,UAAW,kBAAkB;EAC/C,OAAO,QAAQ;EACf,KAAK,SAAS;GAEZ,MAAM,OAAO,KAAK,MAAM,KAAK,UAAU,OAAO,CAAC;GAC/C,YAAY;IACV,IAAI,MAAM,KAAK,MAAM,YAAY,CAAC,GAAG,SAAS,GAAG,SAAS,IAAI;GAChE,CAAC;EACH;CACF;CAEA,MAAM,aAAa,UAAU,QAAQ,OAAO;CAC5C,OAAO;EACL;EACA,WAAW;GACT,KAAK,SAAwB;IAC3B,IAAI,CAAC,MAAM;IACX,MAAM,OAAO,KAAK,MAAM,KAAK,UAAU,OAAO,CAAC;IAC/C,YAAY;KACV,IAAI,MAAM,WAAW,QAAQ,IAAI;IACnC,CAAC;GACH;GACA,UAAU,UAAU;IAClB,UAAU,IAAI,QAAQ;IACtB,aAAa,UAAU,OAAO,QAAQ;GACxC;EACF;EACA,aAAa;GACX,OAAO;GACP,WAAW,WAAW;GACtB,UAAU,MAAM;EAClB;CACF;AACF"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/** What an effect writes for one frame. Effects combine: offsets add up, alphas multiply. */
|
|
2
|
+
export interface EffectOutput {
|
|
3
|
+
offsetX: number;
|
|
4
|
+
offsetY: number;
|
|
5
|
+
alpha: number;
|
|
6
|
+
}
|
|
7
|
+
export type EffectParams = Readonly<Record<string, unknown>>;
|
|
8
|
+
/**
|
|
9
|
+
* A short-lived visual change: an element shaking, blinking. It never goes
|
|
10
|
+
* through the model, so it is not persisted, undoable, nor replayed to late joiners.
|
|
11
|
+
*/
|
|
12
|
+
export interface EffectDefinition<P extends EffectParams = EffectParams> {
|
|
13
|
+
/** Default duration in milliseconds. A `duration` param overrides it. */
|
|
14
|
+
readonly duration: number;
|
|
15
|
+
/** `progress` goes from 0 to 1. */
|
|
16
|
+
update(progress: number, params: P, output: EffectOutput): void;
|
|
17
|
+
}
|
|
18
|
+
/** Left-right oscillation that fades out. Params: `intensity` (pixels), `frequency` (oscillations). */
|
|
19
|
+
export declare const shakeEffect: EffectDefinition;
|
|
20
|
+
/** Fades out and back in. Params: `strength` (0 to 1, how transparent it gets). */
|
|
21
|
+
export declare const blinkEffect: EffectDefinition;
|
|
22
|
+
export declare const builtInEffects: Readonly<Record<string, EffectDefinition>>;
|
|
23
|
+
export type EffectStatus = "completed" | "cancelled"
|
|
24
|
+
/** The element disappeared while the effect was running. */
|
|
25
|
+
| "target-removed" | "unknown-effect" | "target-missing";
|
|
26
|
+
export interface EffectHandle {
|
|
27
|
+
readonly id: number;
|
|
28
|
+
/** Stops the effect and restores the target. No effect if it already ended. */
|
|
29
|
+
cancel(): void;
|
|
30
|
+
readonly finished: Promise<EffectStatus>;
|
|
31
|
+
}
|
|
32
|
+
export interface EffectRunnerOptions {
|
|
33
|
+
/** Whether a target still exists. */
|
|
34
|
+
exists(targetKey: string): boolean;
|
|
35
|
+
/** Receives the combined result for a target each frame, and `null` once its last effect ended. */
|
|
36
|
+
apply(targetKey: string, output: EffectOutput | null): void;
|
|
37
|
+
onWarning?(message: string): void;
|
|
38
|
+
}
|
|
39
|
+
export interface EffectRunner {
|
|
40
|
+
register(name: string, definition: EffectDefinition): void;
|
|
41
|
+
/** Starts an effect. It never throws: an unknown effect or target ends immediately with a status. */
|
|
42
|
+
play(targetKey: string, name: string, params?: EffectParams): EffectHandle;
|
|
43
|
+
/** Advances every running effect by `deltaMs`. Time only moves here, so effects are deterministic. */
|
|
44
|
+
tick(deltaMs: number): void;
|
|
45
|
+
/** Cancels every effect on a target, or on all targets. */
|
|
46
|
+
clear(targetKey?: string): void;
|
|
47
|
+
/** Called when a target is removed from the scene. */
|
|
48
|
+
targetRemoved(targetKey: string): void;
|
|
49
|
+
readonly running: number;
|
|
50
|
+
}
|
|
51
|
+
export declare function createEffectRunner(options: EffectRunnerOptions): EffectRunner;
|
|
52
|
+
//# sourceMappingURL=effects.d.ts.map
|