wenay-common2 1.0.68 → 1.0.70
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 +12 -1
- package/demo/client.ts +100 -0
- package/demo/index.html +28 -0
- package/demo/server.ts +43 -0
- package/doc/ROADMAP.md +9 -3
- package/doc/changes/1.0.69.md +11 -0
- package/doc/changes/1.0.70.md +4 -0
- package/doc/wenay-common2-rare.md +8 -0
- package/doc/wenay-common2.md +42 -6
- package/lib/Common/events/route-signal-webrtc.js +4 -2
- package/lib/Common/peer/peer-client.d.ts +46 -0
- package/lib/Common/peer/peer-client.js +108 -0
- package/lib/Common/peer/peer-host.d.ts +32 -0
- package/lib/Common/peer/peer-host.js +45 -0
- package/lib/Common/peer/peer-index.d.ts +3 -0
- package/lib/Common/peer/peer-index.js +19 -0
- package/lib/Common/peer/peer-relay.d.ts +14 -0
- package/lib/Common/peer/peer-relay.js +77 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +2 -1
- package/observe/PLAN.md +131 -0
- package/observe/README.md +226 -0
- package/observe/hot-write.test.ts +95 -0
- package/observe/listen-core.test.ts +66 -0
- package/observe/listen-store.test.ts +56 -0
- package/observe/listen.test.ts +92 -0
- package/observe/reactive.test.ts +270 -0
- package/observe/reactive.ts +1 -0
- package/observe/store-manager.test.ts +118 -0
- package/observe/store-mirror.example.ts +74 -0
- package/observe/store.test.ts +235 -0
- package/observe/store.ts +1 -0
- package/observe/usage-real-socket.ts +174 -0
- package/observe/usage.ts +200 -0
- package/oracle/PASSED.md +27 -0
- package/oracle/README.md +12 -0
- package/oracle/fixes-primitives.spec.ts +90 -0
- package/oracle/realsocket/_rs.ts +106 -0
- package/oracle/realsocket/auth.spec.ts +91 -0
- package/oracle/realsocket/callbacks.spec.ts +122 -0
- package/oracle/realsocket/caps.spec.ts +124 -0
- package/oracle/realsocket/core.spec.ts +49 -0
- package/oracle/realsocket/dedupe.spec.ts +142 -0
- package/oracle/realsocket/errors.spec.ts +121 -0
- package/oracle/realsocket/lifecycle.spec.ts +128 -0
- package/oracle/realsocket/limits.spec.ts +98 -0
- package/oracle/realsocket/pipe.spec.ts +101 -0
- package/oracle/realsocket/shape.spec.ts +124 -0
- package/oracle/realsocket/slimv2.spec.ts +116 -0
- package/oracle/realsocket/stress.spec.ts +132 -0
- package/oracle/regression/_clientapiall-replay.fixture.ts +57 -0
- package/oracle/regression/async-queues.spec.ts +254 -0
- package/oracle/regression/bytestream.spec.ts +152 -0
- package/oracle/regression/clientapiall-replay-types.spec.ts +47 -0
- package/oracle/regression/core-clone-equal.spec.ts +124 -0
- package/oracle/regression/data-structures.spec.ts +135 -0
- package/oracle/regression/listen-events.spec.ts +206 -0
- package/oracle/regression/observe-core.spec.ts +278 -0
- package/oracle/regression/package-export.spec.ts +120 -0
- package/oracle/regression/rpc-dedupe-callbacks.spec.ts +195 -0
- package/oracle/regression/rpc-lifecycle.spec.ts +150 -0
- package/oracle/regression/store-each.spec.ts +209 -0
- package/package.json +8 -2
- package/replay/PLAN.md +171 -0
- package/replay/canvas-socket.test.ts +187 -0
- package/replay/coalesce.test.ts +260 -0
- package/replay/conflate-socket.test.ts +288 -0
- package/replay/conflate.test.ts +225 -0
- package/replay/history.test.ts +222 -0
- package/replay/media-socket.test.ts +157 -0
- package/replay/offline-store-socket.test.ts +290 -0
- package/replay/offline-store.test.ts +156 -0
- package/replay/peer-sdk.test.ts +228 -0
- package/replay/replay-listen.test.ts +156 -0
- package/replay/route-coordinator.test.ts +314 -0
- package/replay/route-handoff.test.ts +150 -0
- package/replay/route-webrtc.test.ts +321 -0
- package/replay/rpc-auto.test.ts +256 -0
- package/replay/socket-replay.test.ts +199 -0
- package/replay/staleness.test.ts +176 -0
- package/replay/store-replay.test.ts +151 -0
- package/replay/video-socket.demo.ts +200 -0
package/README.md
CHANGED
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
# wenay-common2
|
|
2
2
|
|
|
3
|
+
[](https://github.com/wenayr/wenay-common2/actions/workflows/ci.yml)
|
|
4
|
+
|
|
3
5
|
## Documentation
|
|
4
6
|
|
|
5
7
|
- Brief API cheat sheet: [`doc/wenay-common2.md`](doc/wenay-common2.md)
|
|
6
8
|
- Extended API cheat sheet: [`doc/wenay-common2-rare.md`](doc/wenay-common2-rare.md)
|
|
7
9
|
- Naming migrations: [`doc/NAMING_RENAMES.md`](doc/NAMING_RENAMES.md)
|
|
8
10
|
- Recent changes: [`doc/changes/`](doc/changes/)
|
|
9
|
-
- Project rules for AI/code maintenance: [`CLAUDE.md`](CLAUDE.md)
|
|
11
|
+
- Project rules for AI/code maintenance: [`CLAUDE.md`](CLAUDE.md)
|
|
12
|
+
|
|
13
|
+
## Living examples (shipped in the npm package)
|
|
14
|
+
|
|
15
|
+
- [`demo/`](demo/) — runnable stand (`npm run demo`): shared cursors in two browser tabs over the
|
|
16
|
+
Peer SDK, relay ⇄ WebRTC direct hand-off next to a legacy rpc key.
|
|
17
|
+
- [`replay/`](replay/) · [`observe/`](observe/) · [`oracle/`](oracle/) — the oracle suites CI runs on
|
|
18
|
+
every push; each file doubles as a worked usage example of one subsystem.
|
|
19
|
+
- Note: examples import from the repo's `src/`; in the installed package the same API comes from
|
|
20
|
+
`wenay-common2` / `wenay-common2/peer` / `wenay-common2/replay` / `wenay-common2/observe`.
|
package/demo/client.ts
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
// Demo stand client: shared cursors over the Peer SDK — relay by default,
|
|
2
|
+
// "Go direct" promotes to a real RTCPeerConnection datachannel, "Back to relay"
|
|
3
|
+
// re-interposes. The route hand-off is gap-free by seq; the cursor never jumps.
|
|
4
|
+
import {io} from 'socket.io-client'
|
|
5
|
+
import {createRpcClientHub} from '../src/Common/rcp/rpc-clientHub'
|
|
6
|
+
import {createPeerClient} from '../src/Common/peer/peer-index'
|
|
7
|
+
|
|
8
|
+
type World = {cursor: {x: number, y: number}, color: string, name: string}
|
|
9
|
+
|
|
10
|
+
const params = new URLSearchParams(location.search)
|
|
11
|
+
const me = params.get('me') ?? 'a'
|
|
12
|
+
const other = params.get('peer') ?? (me == 'a' ? 'b' : 'a')
|
|
13
|
+
|
|
14
|
+
const el = (id: string) => document.getElementById(id)!
|
|
15
|
+
const logBox = () => el('log') as HTMLDivElement
|
|
16
|
+
|
|
17
|
+
function log(line: string) {
|
|
18
|
+
const box = logBox()
|
|
19
|
+
const row = document.createElement('div')
|
|
20
|
+
row.textContent = `${new Date().toLocaleTimeString()} ${line}`
|
|
21
|
+
box.prepend(row)
|
|
22
|
+
while (box.children.length > 30) box.lastChild?.remove()
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async function main() {
|
|
26
|
+
document.title = `peer ${me}`
|
|
27
|
+
el('who').textContent = `me: ${me} · watching: ${other}`
|
|
28
|
+
|
|
29
|
+
const hub = createRpcClientHub(
|
|
30
|
+
() => io({transports: ['websocket'], auth: {account: me}}),
|
|
31
|
+
r => ({app: r<any>('app')}) as const,
|
|
32
|
+
)
|
|
33
|
+
const clients = await hub.setToken(null)
|
|
34
|
+
await clients.app.readyStrict()
|
|
35
|
+
log('rpc connected; legacy serverTime() = ' + await clients.app.func.serverTime())
|
|
36
|
+
|
|
37
|
+
// debug tap: every signaling envelope this account receives
|
|
38
|
+
;(clients.app.func.peer.signal.signals as any).on((env: any) => {
|
|
39
|
+
log(`sig<- ${env.type} ${env.from}->${env.to}` +
|
|
40
|
+
(env.sdp ? ` sdp.len=${String(env.sdp).length}` : '') +
|
|
41
|
+
(env.candidate ? ` cand=${JSON.stringify(env.candidate).slice(0, 70)}` : ''))
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const client = createPeerClient<World>({
|
|
45
|
+
remote: clients.app.func.peer,
|
|
46
|
+
account: me,
|
|
47
|
+
initial: {
|
|
48
|
+
cursor: {x: 160, y: 120},
|
|
49
|
+
color: me == 'a' ? '#4f8ef7' : '#f7a44f',
|
|
50
|
+
name: me,
|
|
51
|
+
},
|
|
52
|
+
rtc: () => new RTCPeerConnection(),
|
|
53
|
+
drain: 'micro',
|
|
54
|
+
})
|
|
55
|
+
const peer = client.peer(other)
|
|
56
|
+
client.onRoute(ev => log(`route ${ev.key}: ${ev.from} -> ${ev.to}${ev.reason ? ` (${String(ev.reason)})` : ''}`))
|
|
57
|
+
peer.ready.then(() => log('peer mirror ready (keyframe landed)')).catch(e => log('mirror error: ' + e))
|
|
58
|
+
|
|
59
|
+
// ============== input: own cursor -> own store ==============
|
|
60
|
+
const canvas = el('canvas') as HTMLCanvasElement
|
|
61
|
+
canvas.addEventListener('mousemove', function onMove(e) {
|
|
62
|
+
const r = canvas.getBoundingClientRect()
|
|
63
|
+
client.store.state.cursor = {x: Math.round(e.clientX - r.left), y: Math.round(e.clientY - r.top)}
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
// ============== render loop: own + mirrored cursor ==============
|
|
67
|
+
const ctx = canvas.getContext('2d')!
|
|
68
|
+
function drawCursor(c: {x: number, y: number}, color: string, name: string) {
|
|
69
|
+
ctx.beginPath()
|
|
70
|
+
ctx.arc(c.x, c.y, 8, 0, Math.PI * 2)
|
|
71
|
+
ctx.fillStyle = color
|
|
72
|
+
ctx.fill()
|
|
73
|
+
ctx.fillStyle = '#333'
|
|
74
|
+
ctx.font = '12px sans-serif'
|
|
75
|
+
ctx.fillText(name, c.x + 12, c.y + 4)
|
|
76
|
+
}
|
|
77
|
+
function frame() {
|
|
78
|
+
ctx.clearRect(0, 0, canvas.width, canvas.height)
|
|
79
|
+
const mine = client.store.state
|
|
80
|
+
drawCursor(mine.cursor, mine.color, mine.name + ' (me)')
|
|
81
|
+
const theirs = peer.store.state
|
|
82
|
+
if (theirs?.cursor) drawCursor(theirs.cursor, theirs.color ?? '#999', (theirs.name ?? other) + ` [${peer.route()}]`)
|
|
83
|
+
el('route').textContent = `route: ${peer.route()} · state: ${peer.state()} · seq: ${peer.seq()}`
|
|
84
|
+
requestAnimationFrame(frame)
|
|
85
|
+
}
|
|
86
|
+
frame()
|
|
87
|
+
|
|
88
|
+
// ============== route controls ==============
|
|
89
|
+
el('direct').addEventListener('click', async function goDirect() {
|
|
90
|
+
log('promoteDirect...')
|
|
91
|
+
const res = await peer.promoteDirect({timeoutMs: 8000})
|
|
92
|
+
log(res.ok ? `direct: ok (${res.state})` : `direct failed: ${String(res.reason)}`)
|
|
93
|
+
})
|
|
94
|
+
el('relay').addEventListener('click', async function backToRelay() {
|
|
95
|
+
const res = await peer.reinterposeRelay('manual')
|
|
96
|
+
log(res.ok ? 'back on relay' : `re-interpose failed: ${String(res.reason)}`)
|
|
97
|
+
})
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
main().catch(e => { console.error(e); log('FATAL: ' + e) })
|
package/demo/index.html
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<title>wenay-common2 peer stand</title>
|
|
6
|
+
<style>
|
|
7
|
+
body { font-family: sans-serif; margin: 16px; color: #222; }
|
|
8
|
+
#canvas { border: 1px solid #bbb; border-radius: 6px; background: #fafafa; display: block; }
|
|
9
|
+
#bar { display: flex; gap: 12px; align-items: center; margin: 8px 0; flex-wrap: wrap; }
|
|
10
|
+
#route { font-family: monospace; }
|
|
11
|
+
button { padding: 6px 14px; border-radius: 6px; border: 1px solid #888; background: #fff; cursor: pointer; }
|
|
12
|
+
button:hover { background: #f0f0f0; }
|
|
13
|
+
#log { font-family: monospace; font-size: 12px; margin-top: 8px; max-height: 180px; overflow-y: auto; color: #555; }
|
|
14
|
+
</style>
|
|
15
|
+
</head>
|
|
16
|
+
<body>
|
|
17
|
+
<h3>wenay-common2 — shared cursors (relay ⇄ WebRTC direct)</h3>
|
|
18
|
+
<div id="bar">
|
|
19
|
+
<span id="who">connecting…</span>
|
|
20
|
+
<button id="direct">Go direct</button>
|
|
21
|
+
<button id="relay">Back to relay</button>
|
|
22
|
+
<span id="route"></span>
|
|
23
|
+
</div>
|
|
24
|
+
<canvas id="canvas" width="640" height="360"></canvas>
|
|
25
|
+
<div id="log"></div>
|
|
26
|
+
<script src="client.js"></script>
|
|
27
|
+
</body>
|
|
28
|
+
</html>
|
package/demo/server.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// Demo stand server: the Peer SDK next to a legacy rpc key, static page hosting.
|
|
2
|
+
// Run: npm run demo -> open the two printed URLs in two tabs.
|
|
3
|
+
import express from 'express'
|
|
4
|
+
import {createServer} from 'http'
|
|
5
|
+
import path from 'path'
|
|
6
|
+
import {Server as SocketIOServer} from 'socket.io'
|
|
7
|
+
import {listen} from '../src/Common/events/Listen'
|
|
8
|
+
import {createPeerHost} from '../src/Common/peer/peer-index'
|
|
9
|
+
import {createRpcServerAuto} from '../src/Common/rcp/rpc-server-auto'
|
|
10
|
+
|
|
11
|
+
const PORT = Number(process.env.PORT ?? 8390)
|
|
12
|
+
|
|
13
|
+
const host = createPeerHost()
|
|
14
|
+
const app = express()
|
|
15
|
+
app.use(express.static(path.resolve(__dirname, 'public')))
|
|
16
|
+
app.get('/', (_req, res) => res.sendFile(path.resolve(__dirname, 'public', 'index.html')))
|
|
17
|
+
|
|
18
|
+
const httpServer = createServer(app)
|
|
19
|
+
const ioServer = new SocketIOServer(httpServer)
|
|
20
|
+
|
|
21
|
+
ioServer.on('connection', function onDemoConnection(socket) {
|
|
22
|
+
const account = String(socket.handshake.auth?.account ?? 'anon')
|
|
23
|
+
const peer = host.connection(account)
|
|
24
|
+
const [disconnect, disconnectListen] = listen<[]>()
|
|
25
|
+
socket.on('disconnect', () => { disconnect(); peer.close() })
|
|
26
|
+
createRpcServerAuto({
|
|
27
|
+
socket: {emit: (key, data) => socket.emit(key, data), on: (key, cb) => socket.on(key, cb)},
|
|
28
|
+
socketKey: 'app',
|
|
29
|
+
object: {
|
|
30
|
+
// legacy key on the SAME connection — the SDK does not displace old code
|
|
31
|
+
serverTime: () => new Date().toISOString(),
|
|
32
|
+
peer: peer.fragment,
|
|
33
|
+
},
|
|
34
|
+
disconnectListen,
|
|
35
|
+
})
|
|
36
|
+
console.log(`[demo] ${account} connected`)
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
httpServer.listen(PORT, function onDemoListen() {
|
|
40
|
+
console.log('[demo] shared-cursor stand is up:')
|
|
41
|
+
console.log(` tab A: http://localhost:${PORT}/?me=a&peer=b`)
|
|
42
|
+
console.log(` tab B: http://localhost:${PORT}/?me=b&peer=a`)
|
|
43
|
+
})
|
package/doc/ROADMAP.md
CHANGED
|
@@ -5,7 +5,10 @@
|
|
|
5
5
|
> `createStoreMirror`, and the replay `seq` / `keyframe` / `frame` contract. None of it requires
|
|
6
6
|
> changing the store core; the store stays single-authority (one `seq` sequencer, last-writer-wins
|
|
7
7
|
> per path), and these features are layers or adapters above that.
|
|
8
|
-
> Status: 🔴 not started · 🟡 partial / ongoing · 🧊
|
|
8
|
+
> Status: 🔴 not started · 🟡 partial / ongoing · 🧊 deferred (super-low priority, not forbidden).
|
|
9
|
+
>
|
|
10
|
+
> Current focus is NOT more transport: see `doc/target/library-uplift-tasks.md` (showcase, SDK facade,
|
|
11
|
+
> vertical demo app). Transport items below stay available but rank below everything in that plan.
|
|
9
12
|
|
|
10
13
|
## 0. Distributed Runtime Model
|
|
11
14
|
|
|
@@ -195,7 +198,9 @@ source of truth. Decompose by write topology:
|
|
|
195
198
|
silently drop a concurrent write. This is the one case that genuinely needs CRDT/OT. Design lead:
|
|
196
199
|
wrap a Yjs/Automerge doc as a `RemoteStore`-shaped source and mirror from it — the store↔transport
|
|
197
200
|
decoupling makes this a small adapter, not a rewrite.
|
|
198
|
-
- Status:
|
|
201
|
+
- Status: 🧊 deferred, super-low priority. Prediction layer waits for the demo app to demand and shape
|
|
202
|
+
it (`doc/target/library-uplift-tasks.md` task 4); CRDT adapter reopens only on a real co-write need.
|
|
203
|
+
Partitioned-authority already expressible today.
|
|
199
204
|
|
|
200
205
|
## 4. Data-transfer optimization backlog (ongoing) 🟡
|
|
201
206
|
|
|
@@ -203,4 +208,5 @@ Open-ended transfer/perf work, especially for backend-heavy models. Never "done"
|
|
|
203
208
|
|
|
204
209
|
- Candidates: tighter `frame` condensation per line; delta/patch minimization; binary framing for hot
|
|
205
210
|
paths; batching heuristics beyond the current `pipe` / `space` modes.
|
|
206
|
-
- Status:
|
|
211
|
+
- Status: 🧊 deferred, super-low priority; pick items only as real bottlenecks surface in the SDK/demo
|
|
212
|
+
consumers, not speculatively.
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# 1.0.69
|
|
2
|
+
|
|
3
|
+
- Fix `createWebRtcConnector` / `acceptWebRtcDirect`: `RTCIceCandidate` now rides the signaling wire as its `toJSON()` init — real browser candidate class instances were mangled by transport serialization, so direct never established (caught by the live demo stand, invisible to fakes).
|
|
4
|
+
- Add the demo stand `demo/` (`npm run demo`): shared cursors in two browser tabs over the Peer SDK next to a legacy rpc key — relay mirroring, "Go direct" (real `RTCPeerConnection`), "Back to relay" re-interposition, signaling/route event log. Verified live in Chrome: seq continues across both hand-offs, no keyframe reset, cursor never jumps.
|
|
5
|
+
|
|
6
|
+
- Add the `Peer` namespace (`wenay-common2/peer`) — the one-call SDK over rpc + store + replay + route coordinator, legacy-friendly by design: the server side is an object fragment spread into an EXISTING `createRpcServerAuto` object (old keys untouched), the client side rides the existing connection's deep proxy.
|
|
7
|
+
- `Peer.createPeerHost({authorize?, history?})`: per-account signaling ports + relay journals + a `noStrict` dynamic `peers` feed; `authorize` is the server-side `canExposeEndpoint`; `revoke(pair, accounts)` pushes both parties back to relay.
|
|
8
|
+
- `Peer.createPeerClient({remote, account, initial, rtc?, ...})`: own store published as a patch line; `peer(account)` returns a live mirrored store + route control (`promoteDirect/reinterposeRelay/fallback/block`, `route()`, `ready`, `seq()`); mirrors survive any route change; omitting `rtc` gives a relay-only client.
|
|
9
|
+
- `Peer.createPatchRelayJournal({history?})`: server-side journal of OWNER-sequenced envelopes — relay and direct share the owner's seq space, so a relay <-> direct hand-off is a plain seq resume (no keyframe reset); keyframe is folded server-side, late joiners work while the owner is offline.
|
|
10
|
+
- Add GitHub Actions CI (build + publish gate + full oracle suite on windows-latest) and a README badge.
|
|
11
|
+
- Add oracle `replay/peer-sdk.test.ts`: two accounts over a real Socket.IO/RPC wire next to a legacy rpc key — relay mirroring, direct promotion via injected fake WebRTC runtime, same-seq-space hand-off assertion, server revoke fallback, late joiner from the folded keyframe, loud refusal of `promoteDirect` without an `rtc` factory.
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
# 1.0.70
|
|
2
|
+
|
|
3
|
+
- Ship the living examples in the npm package: the oracle suites (`replay/`, `observe/`, `oracle/`) and the demo stand (`demo/`, without the generated bundle) — each oracle doubles as a worked usage example of one subsystem, kept honest by CI on every push.
|
|
4
|
+
- README: a "Living examples" section with the src-vs-package import note (examples import the repo's `src/`; installed packages use `wenay-common2` / `wenay-common2/peer` / `wenay-common2/replay` / `wenay-common2/observe`).
|
|
@@ -529,6 +529,14 @@ acceptWebRtcDirect({port, rtc, self, serve, accept?}) -> close()
|
|
|
529
529
|
// responder side: on offer, negotiates answer/ICE and serves serve(env) (exposeReplay(...) as is) into
|
|
530
530
|
// the incoming datachannel; accept(env) validates session material and rejects with a loud revoke
|
|
531
531
|
// (the initiator fails fast, not by timeout). Repeated offer for a pair recreates the session.
|
|
532
|
+
Peer.createPatchRelayJournal({history?}) -> {push(env), remote, seq(), snapshot(), close()}
|
|
533
|
+
// server-side mirror of an OWNER-sequenced patch line: push() takes the owner's envelopes VERBATIM
|
|
534
|
+
// (dedup by seq; a root patch with a LOWER seq = owner restart, legitimate reset point), keyframe is
|
|
535
|
+
// folded server-side (late joiners don't need the owner online), frame condenses last-patch-per-path.
|
|
536
|
+
// Owner seq space is the point: relay and direct routes share coordinates -> hand-off is a seq resume.
|
|
537
|
+
// remote is ReplayRemote-shaped and rpc-exposable as is (line is a REAL Listen — the rpc layer detects
|
|
538
|
+
// listen nodes by registry, a hand-rolled {on: cb => ...} wrapper would not stream).
|
|
539
|
+
// The full SDK on top (createPeerHost/createPeerClient) is most-used surface -> wenay-common2.md.
|
|
532
540
|
serveReplayChannel(source, channel) <-> channelReplayRemote(channel) -> ReplayRemote
|
|
533
541
|
// replay wire over ANY ordered string channel (datachannel/MessagePort/worker/pipe): tiny JSON
|
|
534
542
|
// sub/req/res protocol, no RPC core — a direct channel lives OUTSIDE the main rpc connection.
|
package/doc/wenay-common2.md
CHANGED
|
@@ -136,8 +136,8 @@ l.ticks.once(v => console.log(v)) // one event, then aut
|
|
|
136
136
|
const [tick, ticks] = replayListen<[number]>({history: 1024, current: 'last'}) // after — same facade, same key
|
|
137
137
|
// legacy subscribers unchanged (byte-for-byte). Replay consumers now also get:
|
|
138
138
|
const sub = replaySubscribe(l.ticks, v => {}, {since: saved, onSeq: s => saved = s}) // catch-up + live, no gaps/dups
|
|
139
|
-
const sub2 = replaySubscribe(c.math.func.ticks, v => {}) // replay members project on func/strict directly — no cast needed
|
|
140
|
-
const routed = replayRouteSubscribe(l.ticks, v => {}, {label: 'relay'})
|
|
139
|
+
const sub2 = replaySubscribe(c.math.func.ticks, v => {}) // replay members project on func/strict directly — no cast needed
|
|
140
|
+
const routed = replayRouteSubscribe(l.ticks, v => {}, {label: 'relay'})
|
|
141
141
|
await routed.switch(nextRemoteTicks, {label: 'direct'}) // relay/direct hand-off: old route closes after catch-up
|
|
142
142
|
await l.ticks.frame(mySeq) // pull at YOUR pace (50ms timer etc.) — server condenses via the line's frame lambda
|
|
143
143
|
// full guide + examples → rpc.md; frame model / lag policies → 🎞️ recipe below and rare docs
|
|
@@ -152,8 +152,44 @@ Media.createVideoSource({fps? = 3, codec? = 'jpeg', quality?, replay?}) -> [emit
|
|
|
152
152
|
control: start() -> Promise<'idle'|'requesting'|'live'|'denied'|'no-device'|'error'> · stop() · getStats() · setDevice(id) · listDevices() · state
|
|
153
153
|
Media.encodeMediaFrame(meta, payload) / Media.decodeMediaFrame(frame) // one Uint8Array = 40-byte fixed header + raw payload
|
|
154
154
|
```
|
|
155
|
-
Audio default is PCM frames from `AudioWorklet` where available (`mode:'record'` uses MediaRecorder chunks). Video default is camera snapshots (`video->canvas`, JPEG, low fps for vision). Put `listen` into `createRpcServerAuto` like any other Listen; with `replay:true`, the returned listen is a replay line, so RPC auto exposes legacy + replay surfaces under the same key. Backpressure policy: audio is lossless queue; video `replay:true` defaults to keep-latest frame recovery. `transport:'webrtc'` is reserved for a future SFU/signaling adapter; socket binary is the default today.
|
|
156
|
-
|
|
155
|
+
Audio default is PCM frames from `AudioWorklet` where available (`mode:'record'` uses MediaRecorder chunks). Video default is camera snapshots (`video->canvas`, JPEG, low fps for vision). Put `listen` into `createRpcServerAuto` like any other Listen; with `replay:true`, the returned listen is a replay line, so RPC auto exposes legacy + replay surfaces under the same key. Backpressure policy: audio is lossless queue; video `replay:true` defaults to keep-latest frame recovery. `transport:'webrtc'` is reserved for a future SFU/signaling adapter; socket binary is the default today.
|
|
156
|
+
|
|
157
|
+
## 🤝 Peer — accounts see each other's stores (one-call SDK)
|
|
158
|
+
> `import { Peer } from "wenay-common2"` or `import * as Peer from "wenay-common2/peer"`.
|
|
159
|
+
> The happy-path facade over rpc + store + replay + route coordinator. Legacy-friendly by design:
|
|
160
|
+
> the server side is a FRAGMENT spread into your EXISTING `createRpcServerAuto` object, the client
|
|
161
|
+
> side rides your existing connection — old keys keep working untouched.
|
|
162
|
+
```
|
|
163
|
+
// SERVER — next to your legacy object:
|
|
164
|
+
const host = Peer.createPeerHost({authorize?, history?}) // authorize(env) = server-side canExposeEndpoint
|
|
165
|
+
io.on('connection', socket => {
|
|
166
|
+
const peer = host.connection(accountOf(socket)) // per-account signal port + relay journal
|
|
167
|
+
createRpcServerAuto({socket, socketKey, object: {...legacyObject, peer: peer.fragment}, disconnectListen})
|
|
168
|
+
disconnectListen.on(peer.close)
|
|
169
|
+
})
|
|
170
|
+
host: connection(account) · relay(account) · accounts() · revoke(pair, accounts, reason?) · close()
|
|
171
|
+
|
|
172
|
+
// CLIENT — the whole happy path:
|
|
173
|
+
const me = Peer.createPeerClient<World>({
|
|
174
|
+
remote: c.app.func.peer, // deep proxy of the fragment — rest of the connection is yours
|
|
175
|
+
account: 'a',
|
|
176
|
+
initial: {...}, // own store: write me.store.state — others see it
|
|
177
|
+
rtc?: () => new RTCPeerConnection(cfg), // omit = relay-only (promoteDirect unavailable)
|
|
178
|
+
})
|
|
179
|
+
const bob = me.peer('b') // mirror + route control for another account
|
|
180
|
+
await bob.ready // keyframe/tail landed
|
|
181
|
+
bob.store.state // live mirror — reads survive ANY route change
|
|
182
|
+
await bob.promoteDirect() // relay -> WebRTC direct; {ok, state, reason?} result, not a throw
|
|
183
|
+
bob.route() // 'relay' | 'direct' · bob.reinterposeRelay() · bob.fallback() · bob.block()
|
|
184
|
+
me.onRoute(ev => {}) // route transitions for metrics/UI
|
|
185
|
+
```
|
|
186
|
+
Key property: the relay journal stores the owner's envelopes VERBATIM (owner seq space), so a
|
|
187
|
+
relay <-> direct hand-off is a plain seq resume — no keyframe reset, no gaps, no dups. Late joiners
|
|
188
|
+
get a keyframe folded server-side even while the owner is offline. Policy/session material:
|
|
189
|
+
`createPeerClient({session, accept, policy})` + host `authorize` — see rare docs for the envelope
|
|
190
|
+
contract and the underlying primitives (`createRouteCoordinator`, `createSignalHub`,
|
|
191
|
+
`createWebRtcConnector`). Oracle: `replay/peer-sdk.test.ts`.
|
|
192
|
+
|
|
157
193
|
## 🔁 Observe — reactive state + store/mirror API
|
|
158
194
|
> `import { Observe } from "wenay-common2"` or `import * as Observe from "wenay-common2/observe"`.
|
|
159
195
|
> This is the documented v2 reactive/store surface.
|
|
@@ -199,8 +235,8 @@ Observe.exposeStoreReplay(store, {history? = 1024}) -> { api /* spread into the
|
|
|
199
235
|
Observe.syncStoreReplay(mirror, remote /*{line, since, keyframe, frame?} of api.replay*/, {since?, onSeq?}) -> off
|
|
200
236
|
// off.ready (catch-up done) · off.seq() (save for reconnect: syncStoreReplay(..., {since: prev.seq()}))
|
|
201
237
|
// lagging/late client NEVER gets a backlog: evicted seq -> ONE fresh keyframe + live
|
|
202
|
-
// freshness is an option, not consumer boilerplate: {staleMs, onStale} flags a silent line / stale keyframe (edge-triggered both ways; 🎞️ in rare docs)
|
|
203
|
-
Observe.syncStoreReplayRoute(mirror, remote, {label?}) -> off & {switch(nextRemote, opts), ready, seq(), label(), active()}
|
|
238
|
+
// freshness is an option, not consumer boilerplate: {staleMs, onStale} flags a silent line / stale keyframe (edge-triggered both ways; 🎞️ in rare docs)
|
|
239
|
+
Observe.syncStoreReplayRoute(mirror, remote, {label?}) -> off & {switch(nextRemote, opts), ready, seq(), label(), active()}
|
|
204
240
|
// relay/direct promotion and re-interposition: replacement route catches up by seq before the old route closes
|
|
205
241
|
Observe.syncStoreReplayEach<T>(remote, (key, value, ctx) => {}, opts?) -> off & {store, ready, seq(), isStale(), lastTs()}
|
|
206
242
|
// one-call remote fold: mirror store + syncStoreReplay + store.each() — the callback fires per CHANGED
|
|
@@ -130,7 +130,8 @@ function createWebRtcConnector(deps) {
|
|
|
130
130
|
});
|
|
131
131
|
me.onicecandidate = function onIce(ev) {
|
|
132
132
|
if (ev?.candidate != null) {
|
|
133
|
-
|
|
133
|
+
const c = ev.candidate;
|
|
134
|
+
void port.send({ type: 'ice', pair, from: self, to: peer, candidate: c?.toJSON ? c.toJSON() : c });
|
|
134
135
|
}
|
|
135
136
|
};
|
|
136
137
|
try {
|
|
@@ -202,7 +203,8 @@ function acceptWebRtcDirect(deps) {
|
|
|
202
203
|
};
|
|
203
204
|
pc.onicecandidate = function onIce(ev) {
|
|
204
205
|
if (ev?.candidate != null) {
|
|
205
|
-
|
|
206
|
+
const c = ev.candidate;
|
|
207
|
+
void port.send({ type: 'ice', pair: env.pair, from: self, to: env.from, candidate: c?.toJSON ? c.toJSON() : c });
|
|
206
208
|
}
|
|
207
209
|
};
|
|
208
210
|
try {
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { StoreDrain, StorePatch } from '../Observe/store';
|
|
2
|
+
import { ReplayRemote } from '../events/replay-wire';
|
|
3
|
+
import { RoutePolicy } from '../events/route-coordinator';
|
|
4
|
+
import { RtcPeerConnection, SignalEnvelope } from '../events/route-signal-webrtc';
|
|
5
|
+
import { PatchEnvelope } from './peer-relay';
|
|
6
|
+
export type PeerRemote = {
|
|
7
|
+
signal: {
|
|
8
|
+
send: (env: SignalEnvelope) => Promise<boolean | void>;
|
|
9
|
+
signals: {
|
|
10
|
+
on: (cb: (env: SignalEnvelope) => void) => any;
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
publish: (env: PatchEnvelope) => Promise<boolean | void> | boolean | void;
|
|
14
|
+
peers: Record<string, ReplayRemote<[StorePatch]>>;
|
|
15
|
+
};
|
|
16
|
+
export type PeerClientDeps<T extends object> = {
|
|
17
|
+
remote: PeerRemote;
|
|
18
|
+
account: string;
|
|
19
|
+
initial: T;
|
|
20
|
+
rtc?: () => RtcPeerConnection;
|
|
21
|
+
session?: unknown;
|
|
22
|
+
accept?: (env: SignalEnvelope) => boolean | Promise<boolean>;
|
|
23
|
+
policy?: RoutePolicy;
|
|
24
|
+
peerInitial?: () => T;
|
|
25
|
+
history?: number;
|
|
26
|
+
drain?: StoreDrain;
|
|
27
|
+
};
|
|
28
|
+
export declare function createPeerClient<T extends object>(deps: PeerClientDeps<T>): {
|
|
29
|
+
store: import("../Observe/store").Store<T>;
|
|
30
|
+
peer: (other: string) => {
|
|
31
|
+
account: string;
|
|
32
|
+
store: import("../Observe/store").Store<T>;
|
|
33
|
+
ready: Promise<void>;
|
|
34
|
+
seq: () => number;
|
|
35
|
+
route: () => string;
|
|
36
|
+
state: () => import("../events/route-coordinator").tRouteState;
|
|
37
|
+
promoteDirect: (opts?: import("../events/route-coordinator").PromoteDirectOpts) => Promise<import("../events/route-coordinator").RouteOpResult>;
|
|
38
|
+
reinterposeRelay: (reason?: unknown) => Promise<import("../events/route-coordinator").RouteOpResult>;
|
|
39
|
+
fallback: (reason?: unknown) => Promise<import("../events/route-coordinator").RouteOpResult>;
|
|
40
|
+
block: (reason?: unknown) => Promise<import("../events/route-coordinator").RouteOpResult>;
|
|
41
|
+
close(): void;
|
|
42
|
+
};
|
|
43
|
+
onRoute: (cb: (ev: import("../events/route-coordinator").RouteChangeEvent) => void) => import("../..").ListenOff;
|
|
44
|
+
close(): void;
|
|
45
|
+
};
|
|
46
|
+
export type PeerClient<T extends object> = ReturnType<typeof createPeerClient<T>>;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPeerClient = createPeerClient;
|
|
4
|
+
const store_1 = require("../Observe/store");
|
|
5
|
+
const store_replay_1 = require("../Observe/store-replay");
|
|
6
|
+
const replay_wire_1 = require("../events/replay-wire");
|
|
7
|
+
const route_coordinator_1 = require("../events/route-coordinator");
|
|
8
|
+
const route_signal_webrtc_1 = require("../events/route-signal-webrtc");
|
|
9
|
+
function createPeerClient(deps) {
|
|
10
|
+
const { remote, account, initial, rtc, session, accept, policy, peerInitial = () => ({}), history, drain, } = deps;
|
|
11
|
+
const store = (0, store_1.createStore)(initial, drain !== undefined ? { drain } : {});
|
|
12
|
+
const exposed = (0, store_replay_1.exposeStoreReplay)(store, history !== undefined ? { history } : {});
|
|
13
|
+
const offPublish = exposed.replay.line.on(function publishEnvelope(env) {
|
|
14
|
+
void remote.publish(env);
|
|
15
|
+
});
|
|
16
|
+
const warmup = exposed.replay.keyframe();
|
|
17
|
+
if (warmup)
|
|
18
|
+
void remote.publish(warmup);
|
|
19
|
+
const port = {
|
|
20
|
+
send: env => remote.signal.send(env),
|
|
21
|
+
signals: { on: cb => remote.signal.signals.on(cb) },
|
|
22
|
+
};
|
|
23
|
+
const stopAccept = rtc
|
|
24
|
+
? (0, route_signal_webrtc_1.acceptWebRtcDirect)({
|
|
25
|
+
port, rtc, self: account,
|
|
26
|
+
serve: () => (0, replay_wire_1.exposeReplay)(exposed.replay),
|
|
27
|
+
...(accept ? { accept } : {}),
|
|
28
|
+
})
|
|
29
|
+
: null;
|
|
30
|
+
function relayConnector(other) {
|
|
31
|
+
let state = 'idle';
|
|
32
|
+
return {
|
|
33
|
+
info: { label: 'relay', kind: 'relay', ordered: true, reliable: true },
|
|
34
|
+
open() {
|
|
35
|
+
const node = remote.peers[other];
|
|
36
|
+
state = 'open';
|
|
37
|
+
return {
|
|
38
|
+
line: { on: (cb) => node.line.on(cb) },
|
|
39
|
+
since: (seq) => node.since(seq),
|
|
40
|
+
keyframe: () => node.keyframe(),
|
|
41
|
+
frame: (seq, hint) => node.frame(seq, hint),
|
|
42
|
+
};
|
|
43
|
+
},
|
|
44
|
+
close: () => { state = 'closed'; },
|
|
45
|
+
state: () => state,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
const coord = (0, route_coordinator_1.createRouteCoordinator)({
|
|
49
|
+
...(policy ? { policy } : {}),
|
|
50
|
+
connect(ref, kind) {
|
|
51
|
+
const other = ref.a == account ? ref.b : ref.a;
|
|
52
|
+
if (kind == 'relay')
|
|
53
|
+
return relayConnector(other);
|
|
54
|
+
if (!rtc)
|
|
55
|
+
throw new Error('peer client: promoteDirect needs an rtc factory (deps.rtc)');
|
|
56
|
+
return (0, route_signal_webrtc_1.createWebRtcConnector)({
|
|
57
|
+
port, rtc, self: account, peer: other, pair: ref.key, session,
|
|
58
|
+
});
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
const views = new Map();
|
|
62
|
+
function makeView(other) {
|
|
63
|
+
const link = coord.pair(account, other);
|
|
64
|
+
const mirror = (0, store_1.createStore)(peerInitial(), drain !== undefined ? { drain } : {});
|
|
65
|
+
const sub = link.subscribe(function mirrorPatch(patch) {
|
|
66
|
+
(0, store_1.applyStorePatch)(mirror, patch);
|
|
67
|
+
});
|
|
68
|
+
const view = {
|
|
69
|
+
account: other,
|
|
70
|
+
store: mirror,
|
|
71
|
+
ready: sub.ready,
|
|
72
|
+
seq: sub.seq,
|
|
73
|
+
route: link.label,
|
|
74
|
+
state: link.state,
|
|
75
|
+
promoteDirect: link.promoteDirect,
|
|
76
|
+
reinterposeRelay: link.reinterposeRelay,
|
|
77
|
+
fallback: link.fallback,
|
|
78
|
+
block: link.block,
|
|
79
|
+
close() {
|
|
80
|
+
views.delete(other);
|
|
81
|
+
sub();
|
|
82
|
+
link.close();
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
return view;
|
|
86
|
+
}
|
|
87
|
+
function peer(other) {
|
|
88
|
+
const existing = views.get(other);
|
|
89
|
+
if (existing)
|
|
90
|
+
return existing;
|
|
91
|
+
const view = makeView(other);
|
|
92
|
+
views.set(other, view);
|
|
93
|
+
return view;
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
store,
|
|
97
|
+
peer,
|
|
98
|
+
onRoute: coord.onRoute,
|
|
99
|
+
close() {
|
|
100
|
+
for (const view of Array.from(views.values()))
|
|
101
|
+
view.close();
|
|
102
|
+
coord.close();
|
|
103
|
+
stopAccept?.();
|
|
104
|
+
offPublish();
|
|
105
|
+
exposed.close();
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { StorePatch } from '../Observe/store';
|
|
2
|
+
import { ReplayRemote } from '../events/replay-wire';
|
|
3
|
+
import { SignalEnvelope } from '../events/route-signal-webrtc';
|
|
4
|
+
import { PatchEnvelope } from './peer-relay';
|
|
5
|
+
export type PeerHostDeps = {
|
|
6
|
+
authorize?: (env: SignalEnvelope) => boolean | Promise<boolean>;
|
|
7
|
+
history?: number;
|
|
8
|
+
};
|
|
9
|
+
export declare function createPeerHost(deps?: PeerHostDeps): {
|
|
10
|
+
connection: (account: string) => {
|
|
11
|
+
fragment: {
|
|
12
|
+
signal: {
|
|
13
|
+
send: (env: SignalEnvelope) => Promise<boolean>;
|
|
14
|
+
signals: import("../..").ListenApi<[SignalEnvelope]>;
|
|
15
|
+
};
|
|
16
|
+
publish: (env: PatchEnvelope) => boolean;
|
|
17
|
+
peers: Record<string, ReplayRemote<[StorePatch]>>;
|
|
18
|
+
};
|
|
19
|
+
close: () => void;
|
|
20
|
+
};
|
|
21
|
+
relay: (account: string) => {
|
|
22
|
+
push: (env: PatchEnvelope) => boolean;
|
|
23
|
+
remote: ReplayRemote<[StorePatch]>;
|
|
24
|
+
seq: () => number;
|
|
25
|
+
snapshot: () => any;
|
|
26
|
+
close: () => void;
|
|
27
|
+
};
|
|
28
|
+
accounts: () => string[];
|
|
29
|
+
revoke: (pair: string, accounts: string[], reason?: string) => void;
|
|
30
|
+
close(): void;
|
|
31
|
+
};
|
|
32
|
+
export type PeerHost = ReturnType<typeof createPeerHost>;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPeerHost = createPeerHost;
|
|
4
|
+
const rpc_dynamic_1 = require("../rcp/rpc-dynamic");
|
|
5
|
+
const route_signal_webrtc_1 = require("../events/route-signal-webrtc");
|
|
6
|
+
const peer_relay_1 = require("./peer-relay");
|
|
7
|
+
function createPeerHost(deps = {}) {
|
|
8
|
+
const { authorize, history } = deps;
|
|
9
|
+
const hub = (0, route_signal_webrtc_1.createSignalHub)({ authorize });
|
|
10
|
+
const relays = new Map();
|
|
11
|
+
const peersView = (0, rpc_dynamic_1.noStrict)({});
|
|
12
|
+
function ensureRelay(account) {
|
|
13
|
+
let relay = relays.get(account);
|
|
14
|
+
if (!relay) {
|
|
15
|
+
relay = (0, peer_relay_1.createPatchRelayJournal)({ history });
|
|
16
|
+
relays.set(account, relay);
|
|
17
|
+
peersView[account] = relay.remote;
|
|
18
|
+
}
|
|
19
|
+
return relay;
|
|
20
|
+
}
|
|
21
|
+
function connection(account) {
|
|
22
|
+
const port = hub.register(account);
|
|
23
|
+
const mine = ensureRelay(account);
|
|
24
|
+
return {
|
|
25
|
+
fragment: {
|
|
26
|
+
signal: { send: port.send, signals: port.signals },
|
|
27
|
+
publish: (env) => mine.push(env),
|
|
28
|
+
peers: peersView,
|
|
29
|
+
},
|
|
30
|
+
close: () => port.close(),
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
connection,
|
|
35
|
+
relay: ensureRelay,
|
|
36
|
+
accounts: () => Array.from(relays.keys()),
|
|
37
|
+
revoke: hub.revoke,
|
|
38
|
+
close() {
|
|
39
|
+
hub.close();
|
|
40
|
+
for (const relay of relays.values())
|
|
41
|
+
relay.close();
|
|
42
|
+
relays.clear();
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./peer-relay"), exports);
|
|
18
|
+
__exportStar(require("./peer-host"), exports);
|
|
19
|
+
__exportStar(require("./peer-client"), exports);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { ReplayEvent } from '../events/replay-listen';
|
|
2
|
+
import { ReplayRemote } from '../events/replay-wire';
|
|
3
|
+
import { StorePatch } from '../Observe/store';
|
|
4
|
+
export type PatchEnvelope = ReplayEvent<[StorePatch]>;
|
|
5
|
+
export declare function createPatchRelayJournal(opts?: {
|
|
6
|
+
history?: number;
|
|
7
|
+
}): {
|
|
8
|
+
push: (env: PatchEnvelope) => boolean;
|
|
9
|
+
remote: ReplayRemote<[StorePatch]>;
|
|
10
|
+
seq: () => number;
|
|
11
|
+
snapshot: () => any;
|
|
12
|
+
close: () => void;
|
|
13
|
+
};
|
|
14
|
+
export type PatchRelayJournal = ReturnType<typeof createPatchRelayJournal>;
|