@benev/archimedes 0.1.0-1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (133) hide show
  1. package/LICENSE +23 -0
  2. package/README.md +19 -0
  3. package/package.json +44 -0
  4. package/s/_archive/GLOSSARY.md +31 -0
  5. package/s/_archive/README.md +209 -0
  6. package/s/_archive/core/authority.ts +62 -0
  7. package/s/_archive/core/contact/codecs.i.ts +3 -0
  8. package/s/_archive/core/contact/codecs.ts +14 -0
  9. package/s/_archive/core/contact/contact.test.ts +32 -0
  10. package/s/_archive/core/contact/contact.ts +63 -0
  11. package/s/_archive/core/contact/types.ts +24 -0
  12. package/s/_archive/core/contact/wiring.i.ts +3 -0
  13. package/s/_archive/core/contact/wiring.ts +212 -0
  14. package/s/_archive/core/fiber.ts +94 -0
  15. package/s/_archive/core/liaison.ts +73 -0
  16. package/s/_archive/core/parcels/inbox.ts +83 -0
  17. package/s/_archive/core/parcels/parceller.ts +20 -0
  18. package/s/_archive/core/parcels/parcels.test.ts +136 -0
  19. package/s/_archive/core/parcels/types.ts +5 -0
  20. package/s/_archive/core/parcels/utils/nanny.ts +23 -0
  21. package/s/_archive/core/simulator.ts +11 -0
  22. package/s/_archive/core/speculator.ts +63 -0
  23. package/s/_archive/core/types.ts +32 -0
  24. package/s/_archive/core/utils/handle-telegrams.ts +34 -0
  25. package/s/_archive/core/utils/is-input-dispatch.ts +10 -0
  26. package/s/_archive/core/utils/make-telegram-for-inputs.ts +12 -0
  27. package/s/_archive/core/utils/on-channel-message.ts +11 -0
  28. package/s/_archive/eureka/eureka.ts +20 -0
  29. package/s/_archive/eureka/index.ts +0 -0
  30. package/s/_archive/eureka/integration/context.ts +9 -0
  31. package/s/_archive/eureka/integration/simulator.ts +75 -0
  32. package/s/_archive/eureka/integration/types.ts +12 -0
  33. package/s/_archive/eureka/integration/utils/inputs.ts +24 -0
  34. package/s/_archive/eureka/integration/utils/relevance.ts +10 -0
  35. package/s/_archive/eureka/parts/entity.ts +37 -0
  36. package/s/_archive/eureka/parts/system.ts +38 -0
  37. package/s/_archive/eureka/parts/types.ts +21 -0
  38. package/s/_archive/eureka/parts/world.ts +140 -0
  39. package/s/_archive/eureka/testing/eureka.test.ts +27 -0
  40. package/s/_archive/eureka/testing/integration.test.ts +40 -0
  41. package/s/_archive/eureka/testing/situations/health.ts +43 -0
  42. package/s/_archive/eureka/testing/situations/integration.ts +45 -0
  43. package/s/_archive/index.ts +6 -0
  44. package/s/_archive/session/client.ts +56 -0
  45. package/s/_archive/session/host.ts +71 -0
  46. package/s/_archive/session/meta/meta-client.ts +5 -0
  47. package/s/_archive/session/meta/meta-host.ts +18 -0
  48. package/s/_archive/session/meta/types.ts +15 -0
  49. package/s/_archive/session/parts/client-on.ts +8 -0
  50. package/s/_archive/session/parts/fiber-rpc.ts +28 -0
  51. package/s/_archive/session/parts/host-on.ts +9 -0
  52. package/s/_archive/session/parts/hub.ts +40 -0
  53. package/s/_archive/session/parts/netfibers.ts +14 -0
  54. package/s/_archive/session/parts/seat.ts +35 -0
  55. package/s/_archive/session/parts/spoke.ts +10 -0
  56. package/s/_archive/sugar/easy-host.ts +68 -0
  57. package/s/_archive/tests.test.ts +17 -0
  58. package/s/_archive/tools/averager.ts +29 -0
  59. package/s/_archive/tools/bucket.ts +17 -0
  60. package/s/_archive/tools/chronicle.ts +32 -0
  61. package/s/_archive/tools/disposers.ts +5 -0
  62. package/s/_archive/tools/id-counter.ts +13 -0
  63. package/s/_archive/tools/loop.ts +12 -0
  64. package/s/_archive/tools/pub.ts +22 -0
  65. package/s/_archive/tools/ticker.ts +22 -0
  66. package/s/_archive/tools/u8.ts +7 -0
  67. package/s/_archive/transports/sparrow/fibers.ts +19 -0
  68. package/s/_archive/transports/sparrow/start-sparrow-host.ts +26 -0
  69. package/s/_archive/transports/sparrow/utils/netfibers-from-cable.ts +10 -0
  70. package/s/ecs/index.ts +7 -0
  71. package/s/ecs/parts/changers.ts +16 -0
  72. package/s/ecs/parts/make-id.ts +7 -0
  73. package/s/ecs/parts/world.ts +68 -0
  74. package/s/ecs/test/setup-example-world.ts +42 -0
  75. package/s/ecs/test/setup-lifecycle-counts.ts +17 -0
  76. package/s/ecs/test.ts +99 -0
  77. package/s/ecs/types.ts +19 -0
  78. package/s/ecs/utils/is-match.ts +7 -0
  79. package/s/ecs/utils/optimizer.ts +38 -0
  80. package/s/index.ts +3 -0
  81. package/s/net/test.ts +9 -0
  82. package/s/net/types.ts +1 -0
  83. package/s/sim/test.ts +9 -0
  84. package/s/sim/types.ts +1 -0
  85. package/s/test.ts +8 -0
  86. package/x/ecs/index.d.ts +4 -0
  87. package/x/ecs/index.js +5 -0
  88. package/x/ecs/index.js.map +1 -0
  89. package/x/ecs/parts/changers.d.ts +4 -0
  90. package/x/ecs/parts/changers.js +11 -0
  91. package/x/ecs/parts/changers.js.map +1 -0
  92. package/x/ecs/parts/make-id.d.ts +1 -0
  93. package/x/ecs/parts/make-id.js +5 -0
  94. package/x/ecs/parts/make-id.js.map +1 -0
  95. package/x/ecs/parts/world.d.ts +11 -0
  96. package/x/ecs/parts/world.js +59 -0
  97. package/x/ecs/parts/world.js.map +1 -0
  98. package/x/ecs/test/setup-example-world.d.ts +10 -0
  99. package/x/ecs/test/setup-example-world.js +31 -0
  100. package/x/ecs/test/setup-example-world.js.map +1 -0
  101. package/x/ecs/test/setup-lifecycle-counts.d.ts +6 -0
  102. package/x/ecs/test/setup-lifecycle-counts.js +15 -0
  103. package/x/ecs/test/setup-lifecycle-counts.js.map +1 -0
  104. package/x/ecs/test.d.ts +13 -0
  105. package/x/ecs/test.js +85 -0
  106. package/x/ecs/test.js.map +1 -0
  107. package/x/ecs/types.d.ts +12 -0
  108. package/x/ecs/types.js +2 -0
  109. package/x/ecs/types.js.map +1 -0
  110. package/x/ecs/utils/is-match.d.ts +2 -0
  111. package/x/ecs/utils/is-match.js +4 -0
  112. package/x/ecs/utils/is-match.js.map +1 -0
  113. package/x/ecs/utils/optimizer.d.ts +9 -0
  114. package/x/ecs/utils/optimizer.js +37 -0
  115. package/x/ecs/utils/optimizer.js.map +1 -0
  116. package/x/index.d.ts +1 -0
  117. package/x/index.js +2 -0
  118. package/x/index.js.map +1 -0
  119. package/x/net/test.d.ts +4 -0
  120. package/x/net/test.js +7 -0
  121. package/x/net/test.js.map +1 -0
  122. package/x/net/types.d.ts +1 -0
  123. package/x/net/types.js +2 -0
  124. package/x/net/types.js.map +1 -0
  125. package/x/sim/test.d.ts +4 -0
  126. package/x/sim/test.js +7 -0
  127. package/x/sim/test.js.map +1 -0
  128. package/x/sim/types.d.ts +1 -0
  129. package/x/sim/types.js +2 -0
  130. package/x/sim/types.js.map +1 -0
  131. package/x/test.d.ts +1 -0
  132. package/x/test.js +6 -0
  133. package/x/test.js.map +1 -0
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+
2
+ MIT License
3
+
4
+ Copyright (c) 2026 Chase Moskal
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
23
+
package/README.md ADDED
@@ -0,0 +1,19 @@
1
+
2
+ ![](https://i.imgur.com/JNCvW1J.png)
3
+
4
+ # 🏛️ archimedes netlogic engine
5
+
6
+ > [***"do not disturb my circles!"***](https://en.wikipedia.org/wiki/noli_turbare_circulos_meos!)
7
+ >     — *archimedes, c. 212 bc*
8
+
9
+ ## rollforward netcode for web games
10
+
11
+ 🔮 **automatic networking**
12
+ you just code your game naively as though it's singleplayer, archimedes handles the multiplayer.
13
+
14
+ 🌎 **whole-world rollforward**
15
+ clientside prediction applies to the whole simulation. all effects of player inputs feel instant.
16
+
17
+ 🧩 **ecs architecture**
18
+ program your simulation with entities, components, and systems.
19
+
package/package.json ADDED
@@ -0,0 +1,44 @@
1
+ {
2
+ "name": "@benev/archimedes",
3
+ "version": "0.1.0-1",
4
+ "description": "game ecs with auto networking",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "x/index.js",
8
+ "files": [
9
+ "x",
10
+ "s"
11
+ ],
12
+ "scripts": {
13
+ "build": "octo-s 'rm -rf x' 'mkdir x' 'tsc'",
14
+ "dev": "octo 'node --watch x/test.js' 'tsc -w'",
15
+ "test": "node x/test.js",
16
+ "count": "find s -path '*/_archive' -prune -o -name '*.ts' -exec wc -l {} +"
17
+ },
18
+ "dependencies": {
19
+ "@e280/renraku": "^0.5.7",
20
+ "@e280/stz": "^0.2.27",
21
+ "@msgpack/msgpack": "^3.1.3",
22
+ "sparrow-rtc": "^0.2.15"
23
+ },
24
+ "devDependencies": {
25
+ "@e280/octo": "^0.1.0",
26
+ "@e280/science": "^0.1.9",
27
+ "npm-run-all": "^4.1.5",
28
+ "typescript": "^6.0.2"
29
+ },
30
+ "author": "Chase Moskal <chasemoskal@gmail.com>",
31
+ "keywords": [
32
+ "networking",
33
+ "rollforward",
34
+ "web-games"
35
+ ],
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "git+https://github.com/benevolent-games/archimedes.git"
39
+ },
40
+ "bugs": {
41
+ "url": "https://github.com/benevolent-games/archimedes/issues"
42
+ },
43
+ "homepage": "https://github.com/benevolent-games/archimedes#readme"
44
+ }
@@ -0,0 +1,31 @@
1
+
2
+ ## Archimedes Concepts
3
+
4
+ ### Core concepts
5
+
6
+ **Simulator**
7
+ Abstract class that holds a game state and a `simulate` method. Also has a `tailor` method which can extract a subset of the state for a specific author.
8
+ - *Author ID*, clarifies who an input is coming from. Host alwaysg gets authorId `0`, each connected client gets a different number.
9
+ - *Telegram*, stream of information about the changing state of a simulation. Includes dispatches about state, delta, and input.
10
+ - *Schema*, the types that a simulator operates on.
11
+ - `state`, full representation of the game state.
12
+ - `delta`, partial game state, reprents an update/patch to be applied to the state.
13
+ - `input`, data that clients send back to the host, which may influence the simulation.
14
+
15
+ **Liaison**
16
+ Symmetric comms channel, which handles ping/pongs, jitter-smoothing, inbox and outbox. Clients and hosts use a liaison.
17
+
18
+ **Authority**
19
+ Hosting rig. Wires a simulator up with a liaison, providing a tick method that handles all communications.
20
+
21
+ **Speculator**
22
+ Client rig. Bundles up a pastSimulator and a futureSimulator, and coordinates with a liaison to implement roll-forward networking. Clientside should render the state of the futureSimulator, as it represents the clientside prediction.
23
+
24
+ ### Session concepts
25
+
26
+ **SessionHost**
27
+ Creates an Authority and coordinates metadata and userland api setup.
28
+
29
+ **SessionClient**
30
+ Creates a Speculator and coordinates metadata and userland api setup.
31
+
@@ -0,0 +1,209 @@
1
+
2
+ ![](https://i.imgur.com/JNCvW1J.png)
3
+
4
+ # 🏛️ Archimedes
5
+
6
+ > [***"Do not disturb my circles!"***](https://en.wikipedia.org/wiki/Noli_turbare_circulos_meos!)
7
+ > &nbsp; &nbsp; — *Archimedes, c. 212 BC*
8
+
9
+ ## Tournament-grade rollforward netcode for web games
10
+
11
+ 🔮 **Whole-world rollforward:**
12
+ Player inputs feel instant, and mispredictions correct themselves. It's all automatic, so you don't even have to think about lag when you're coding your game's logic.
13
+
14
+ 🚚 **Transport-agnostic:**
15
+ Default to free player-hosted games via [Sparrow RTC](https://github.com/benevolent-games/sparrow), but you can swap in webtransport or websockets if you want.
16
+
17
+ ⛵ **Logic-agnostic:**
18
+ We don't care if your game has fancy-schmancy ECS architecture, or a humble classical setup, whatever: just subclass `Simulator` and it's smooth sailing.
19
+
20
+ 🛟 **Seamless reconnects:**
21
+ Users *will* accidentally close their browser tab mid-game. Archimedes helps them pick up where they left off, no harm, no foul.
22
+
23
+ 📦 **Npm package `@benev/archimedes`:**
24
+ Archimedes isn't finished yet, it's under active development.
25
+
26
+ <br/>
27
+
28
+ # 🤯 Eureka
29
+
30
+ > [***"Eureka! I have found it!"***](https://en.wikipedia.org/wiki/Eureka_%28word%29)
31
+ > &nbsp; &nbsp; — *Archimedes, sometime before 212 BC*
32
+
33
+ ## An elegant ECS framework
34
+
35
+ *Eureka* is an Entity-Component-System framework. It was designed alongside Archimedes, and it's the easiest way to get into Archimedes without making a custom Simulator integration. Eureka can be used without Archimedes, and Archimedes can be used without Eureka — but when they're together, it's *a vibe.*
36
+
37
+ ### Eureka quick setup
38
+
39
+ - Define your context.
40
+ ```ts
41
+ // All your systems will have access to this.
42
+ export class MyContext {}
43
+ ```
44
+ - Declare all your components.
45
+ ```ts
46
+ // Each component can be any serializable data.
47
+ export type MyComponents = {
48
+ health: number
49
+ bleeding: number
50
+ mana: number
51
+ manaRegen: number
52
+ }
53
+ ```
54
+ - Setup the Eureka helper.
55
+ It has your context and component types baked-in.
56
+ ```ts
57
+ import {setupEureka} from "@benev/archimedes"
58
+
59
+ export const eureka = setupEureka<MyContext, MyComponents>()
60
+ ```
61
+ - Create the world, and declare your systems.
62
+ Each system selects entities by their components, and runs behaviors on them.
63
+ ```ts
64
+ // Instance your context.
65
+ const context = new MyContext()
66
+
67
+ // Create the world, providing context and an array of systems.
68
+ const world = eureka.world(context, [
69
+
70
+ // Give the system a friendly name.
71
+ eureka.system("my health system")
72
+
73
+ // We select which components this system operates on.
74
+ .select("health").andMaybe("bleeding")
75
+
76
+ // Behavior logic for this system.
77
+ .fn((entities, world) => {
78
+ for (const {id, components} of entities) {
79
+
80
+ // process bleeding
81
+ if (components.bleeding)
82
+ components.health -= components.bleeding
83
+
84
+ // process death
85
+ if (components.health <= 0)
86
+ world.delete(id)
87
+ }
88
+ }),
89
+
90
+ eureka.system("mana regeneration")
91
+ .select("mana", "manaRegen")
92
+ .fn((entities, _world) => {
93
+ for (const {components} of entities)
94
+ components.mana += components.manaRegen
95
+ }),
96
+ ])
97
+ ```
98
+ - Systems are executed sequentially, from top to bottom.
99
+ - You can organize your systems into separate files, of course.
100
+ - Please notice that I painstakingly designed all of this for immaculate typescript typings. You are welcome 😌
101
+
102
+ ### Running a Eureka world that does stuff
103
+
104
+ Okay, now it's time to put something into the world and watch something happen.
105
+
106
+ - Add an entity.
107
+ ```ts
108
+ const warrior = world.create({health: 100, bleeding: 1})
109
+ ```
110
+ - Execute world systems (one simulation tick).
111
+ ```ts
112
+ world.execute()
113
+
114
+ // we see that the bleeding behavior worked.
115
+ warrior.components.health // 99
116
+ ```
117
+ - Alright, that's the basics, go ahead and setup a 60hz tickloop.
118
+ ```ts
119
+ setInterval(() => world.execute(), 1000 / 60)
120
+ ```
121
+
122
+ ### Entity method reference
123
+
124
+ - `entity.id` — the id number for this entity.
125
+ ```ts
126
+ entity.id // 123
127
+ ```
128
+ - `entity.components` — the components object for this entity.
129
+ ```ts
130
+ entity.components.health // 99
131
+ entity.components.bleeding // 1
132
+ ```
133
+ - The components object is a proxy, and setting its properties automatically informs the eureka about the change.
134
+ ```ts
135
+ entity.components.bleeding = 0 // change auto-detected
136
+ entity.components.health += 25 // change auto-detected
137
+ ```
138
+ - You can also delete components, and that'll be detected
139
+ ```ts
140
+ delete entity.components.bleeding // auto-detected
141
+ ```
142
+ - If you want to change a component's shape in terms of components, you can do this:
143
+ ```ts
144
+ entity.components.health // unhappy typescript, entity doesn't have health
145
+
146
+ // updating the entity
147
+ const entityWithHealth = world.update(entity.id, {
148
+ ...entity.components,
149
+ health: 100,
150
+ })
151
+
152
+ entityWithHealth.components.health // happy typescript, knows it has health
153
+
154
+ // note, this is really a typescript ergonomics thing
155
+ entity === entityWithHealth // true
156
+ ```
157
+ - `entity.write()` — manually inform eureka that you've made changes inside the entity's components.
158
+ ```ts
159
+ entity.components.myObject.health += 1 // change inside object, not auto-detected
160
+ entity.write() // manually tell eureka about the change
161
+ ```
162
+
163
+ ### World method reference
164
+
165
+ Creating and updating entities.
166
+ - `world.create(components)` — a new entity.
167
+ ```ts
168
+ const wizard = world.create({health: 100, mana: 50, manaRegen: 1})
169
+ ```
170
+ - `world.update(id, components)` — create or update an entity.
171
+ ```ts
172
+ world.write(wizard.id, {health: 100, mana: 75, manaRegen: 1})
173
+ ```
174
+
175
+ Getting entities.
176
+ - `world.get(id)` — get an entity by id, or return undefined if not present.
177
+ ```ts
178
+ const entity = world.get(id)
179
+ ```
180
+ - `world.require(id)` — get an entity by id, or throw an error if not present.
181
+ ```ts
182
+ const entity = world.require(id)
183
+ ```
184
+ - `world.all()` — iterate over all entities
185
+ ```ts
186
+ for (const entity of world.all)
187
+ console.log(entity.id)
188
+ ```
189
+
190
+ Removing entities.
191
+ - `world.delete(id)` — remove an entity from the world.
192
+ ```ts
193
+ world.delete(id)
194
+ ```
195
+ - `world.clear()` — remove *everything* from the world.
196
+ ```ts
197
+ world.clear()
198
+ ```
199
+
200
+ Full-world data operations.
201
+ - `world.data()` — iterate over all entity data.
202
+ ```ts
203
+ const data = [...world.data()]
204
+ ```
205
+ - `world.overwrite(data)` — create or update all entities with the provided data.
206
+ ```ts
207
+ const entity = world.overwrite(data)
208
+ ```
209
+
@@ -0,0 +1,62 @@
1
+
2
+ import {Liaison} from "./liaison.js"
3
+ import {Simulator} from "./simulator.js"
4
+ import {Ticker} from "../tools/ticker.js"
5
+ import {IdCounter} from "../tools/id-counter.js"
6
+ import {isInputDispatch} from "./utils/is-input-dispatch.js"
7
+ import {DeltaDispatch, InputDispatch, Schema, StateDispatch, Telegram} from "./types.js"
8
+
9
+ export class Authority<xSchema extends Schema> {
10
+ idCounter = new IdCounter()
11
+ liaisons = new Set<Liaison<Telegram<xSchema>>>()
12
+ authorId = this.idCounter.next()
13
+ currentTick = 0
14
+
15
+ constructor(public simulator: Simulator<xSchema>) {}
16
+
17
+ tick() {
18
+ const tick = ++this.currentTick
19
+ const inputDispatches = this.#collectInputDispatches()
20
+ const delta = this.simulator.simulate([tick, inputDispatches])
21
+ const deltaDispatch: DeltaDispatch<xSchema> = ["delta", delta]
22
+
23
+ const broadcast: Telegram<xSchema> = [
24
+ tick,
25
+ [...inputDispatches, deltaDispatch],
26
+ ]
27
+
28
+ for (const liaison of this.liaisons) {
29
+ const tailored = this.simulator.tailor(liaison.authorId, broadcast)
30
+ liaison.send(tailored)
31
+ }
32
+ }
33
+
34
+ makeTicker(hz: number) {
35
+ return new Ticker(hz, () => this.tick())
36
+ }
37
+
38
+ getStateTelegram(): Telegram<xSchema> {
39
+ const dispatch: StateDispatch<xSchema> = ["state", this.simulator.state]
40
+ return [this.authorId, [dispatch]]
41
+ }
42
+
43
+ #collectInputDispatches() {
44
+ const dispatches: InputDispatch<xSchema>[] = []
45
+
46
+ for (const liaison of this.liaisons)
47
+ liaison.recv()
48
+ .map(([_, dispatches]) => ["input", [
49
+
50
+ // overwrite author id to prevent spoofing
51
+ liaison.authorId,
52
+
53
+ // filter for inputs
54
+ dispatches.filter(isInputDispatch),
55
+
56
+ ]] as InputDispatch<xSchema>)
57
+ .forEach(t => dispatches.push(t))
58
+
59
+ return dispatches
60
+ }
61
+ }
62
+
@@ -0,0 +1,3 @@
1
+
2
+ export * as codecs from "./codecs.js"
3
+
@@ -0,0 +1,14 @@
1
+
2
+ import * as m from "@msgpack/msgpack"
3
+ import {asCodec} from "./types.js"
4
+
5
+ export const json = asCodec({
6
+ encode: <D>(data: D) => new TextEncoder().encode(JSON.stringify(data)),
7
+ decode: <D>(code: Uint8Array) => JSON.parse(new TextDecoder().decode(code)) as D,
8
+ })
9
+
10
+ export const msgpack = asCodec({
11
+ encode: <D>(data: D) => m.encode(data),
12
+ decode: <D>(code: Uint8Array) => m.decode(code) as D,
13
+ })
14
+
@@ -0,0 +1,32 @@
1
+
2
+ import {coalesce} from "@e280/stz"
3
+ import {Science, test, expect, spy} from "@e280/science"
4
+
5
+ import {Contact} from "./contact.js"
6
+ import {wiring} from "./wiring.i.js"
7
+
8
+ export default Science.suite({
9
+ "binary exchange": test(async() => {
10
+ const alice = new Contact<string, number>()
11
+ const bob = new Contact<Uint8Array>()
12
+ const charlie = new Contact<Uint8Array>()
13
+ const debbie = new Contact<number, string>()
14
+
15
+ const stop = coalesce(
16
+ wiring.relayBinary(alice, bob),
17
+ wiring.relayBinary(debbie, charlie),
18
+ wiring.exchange(bob, charlie),
19
+ )
20
+
21
+ const debbieRecv = spy((_: number) => {})
22
+ debbie.recv.on(debbieRecv)
23
+ expect(debbieRecv.spy.calls.length).is(0)
24
+
25
+ alice.send(123, true)
26
+ expect(debbieRecv.spy.calls.length).is(1)
27
+ expect(debbieRecv.spy.calls.at(0)!.args[0]).is(123)
28
+
29
+ stop()
30
+ }),
31
+ })
32
+
@@ -0,0 +1,63 @@
1
+
2
+ import {pub} from "@e280/stz"
3
+ import {StdCable} from "sparrow-rtc"
4
+
5
+ import {json} from "./codecs.js"
6
+ import {ContactInput, Codec} from "./types.js"
7
+ import {exchange, mirror, relayCable} from "./wiring.js"
8
+
9
+ export class Contact<I = any, O = I> {
10
+ send = pub<[output: O, reliable: boolean]>()
11
+ recv = pub<[input: I, reliable: boolean]>()
12
+
13
+ /** clear all send and recv listeners */
14
+ dispose() {
15
+ this.send.clear()
16
+ this.recv.clear()
17
+ }
18
+
19
+ /** create a new contact that is an exchange partner (see wiring.exchange) */
20
+ exchange() {
21
+ const bob = new Contact<O, I>()
22
+ const detach = exchange(this, bob)
23
+ return [bob, detach] as [typeof bob, typeof detach]
24
+ }
25
+
26
+ /** create a new contact that is a mirror (see wiring.mirror) */
27
+ mirror() {
28
+ const bob = new Contact<I, O>()
29
+ const detach = mirror(this, bob)
30
+ return [bob, detach] as [typeof bob, typeof detach]
31
+ }
32
+
33
+ /** create a new contact that is a relay (see wiring.relay) */
34
+ relay() {
35
+ const bob = new Contact<I, O>()
36
+ const detach = mirror(this, bob)
37
+ return [bob, detach] as [typeof bob, typeof detach]
38
+ }
39
+
40
+ /** attach an rtc cable to this contact as a relay */
41
+ relayCable(cable: StdCable, codec: Codec = json) {
42
+ return relayCable(this, cable, codec)
43
+ }
44
+
45
+ /** roll multiple sub-contacts into a single mega-contact */
46
+ static multiplex<Sc extends {[key: string]: Contact}>(subcontacts: Sc) {
47
+ type Submessages = {[K in keyof Sc]: [K, ContactInput<Sc[K]>]}
48
+ type Submessage = Submessages[keyof Sc]
49
+ const megacontact = new Contact<Submessage>()
50
+
51
+ for (const [key, subcontact] of Object.entries(subcontacts))
52
+ subcontact.send.on((x, reliable) => megacontact.send([key, x], reliable))
53
+
54
+ megacontact.recv.on(([key, data], reliable) => {
55
+ const subcontact = subcontacts[key as any]
56
+ if (!subcontact) throw new Error(`unknown subcontact "${key as any}"`)
57
+ subcontact.recv(data as any, reliable)
58
+ })
59
+
60
+ return megacontact
61
+ }
62
+ }
63
+
@@ -0,0 +1,24 @@
1
+
2
+ import {Contact} from "./contact.js"
3
+
4
+ /** infer a contact's input data type */
5
+ export type ContactInput<C extends Contact> = (
6
+ C extends Contact<infer I>
7
+ ? I
8
+ : never
9
+ )
10
+
11
+ /** infer a contact's output data type */
12
+ export type ContactOutput<C extends Contact> = (
13
+ C extends Contact<any, infer O>
14
+ ? O
15
+ : never
16
+ )
17
+
18
+ export type Codec = {
19
+ encode: <Data>(data: Data) => Uint8Array
20
+ decode: <Data>(code: Uint8Array) => Data
21
+ }
22
+
23
+ export const asCodec = (t: Codec) => t
24
+
@@ -0,0 +1,3 @@
1
+
2
+ export * as wiring from "./wiring.js"
3
+