@nebutra/collab 0.2.2 → 0.2.3

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.
@@ -1,235 +0,0 @@
1
-
2
- <!doctype html>
3
- <html lang="en">
4
-
5
- <head>
6
- <title>Code coverage report for src/store/memory.ts</title>
7
- <meta charset="utf-8" />
8
- <link rel="stylesheet" href="../../prettify.css" />
9
- <link rel="stylesheet" href="../../base.css" />
10
- <link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
11
- <meta name="viewport" content="width=device-width, initial-scale=1" />
12
- <style type='text/css'>
13
- .coverage-summary .sorter {
14
- background-image: url(../../sort-arrow-sprite.png);
15
- }
16
- </style>
17
- </head>
18
-
19
- <body>
20
- <div class='wrapper'>
21
- <div class='pad1'>
22
- <h1><a href="../../index.html">All files</a> / <a href="index.html">src/store</a> memory.ts</h1>
23
- <div class='clearfix'>
24
-
25
- <div class='fl pad1y space-right2'>
26
- <span class="strong">80% </span>
27
- <span class="quiet">Statements</span>
28
- <span class='fraction'>4/5</span>
29
- </div>
30
-
31
-
32
- <div class='fl pad1y space-right2'>
33
- <span class="strong">100% </span>
34
- <span class="quiet">Branches</span>
35
- <span class='fraction'>2/2</span>
36
- </div>
37
-
38
-
39
- <div class='fl pad1y space-right2'>
40
- <span class="strong">66.66% </span>
41
- <span class="quiet">Functions</span>
42
- <span class='fraction'>2/3</span>
43
- </div>
44
-
45
-
46
- <div class='fl pad1y space-right2'>
47
- <span class="strong">80% </span>
48
- <span class="quiet">Lines</span>
49
- <span class='fraction'>4/5</span>
50
- </div>
51
-
52
-
53
- </div>
54
- <p class="quiet">
55
- Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
56
- </p>
57
- <template id="filterTemplate">
58
- <div class="quiet">
59
- Filter:
60
- <input type="search" id="fileSearch">
61
- </div>
62
- </template>
63
- </div>
64
- <div class='status-line high'></div>
65
- <pre><table class="coverage">
66
- <tr><td class="line-count quiet"><a name='L1'></a><a href='#L1'>1</a>
67
- <a name='L2'></a><a href='#L2'>2</a>
68
- <a name='L3'></a><a href='#L3'>3</a>
69
- <a name='L4'></a><a href='#L4'>4</a>
70
- <a name='L5'></a><a href='#L5'>5</a>
71
- <a name='L6'></a><a href='#L6'>6</a>
72
- <a name='L7'></a><a href='#L7'>7</a>
73
- <a name='L8'></a><a href='#L8'>8</a>
74
- <a name='L9'></a><a href='#L9'>9</a>
75
- <a name='L10'></a><a href='#L10'>10</a>
76
- <a name='L11'></a><a href='#L11'>11</a>
77
- <a name='L12'></a><a href='#L12'>12</a>
78
- <a name='L13'></a><a href='#L13'>13</a>
79
- <a name='L14'></a><a href='#L14'>14</a>
80
- <a name='L15'></a><a href='#L15'>15</a>
81
- <a name='L16'></a><a href='#L16'>16</a>
82
- <a name='L17'></a><a href='#L17'>17</a>
83
- <a name='L18'></a><a href='#L18'>18</a>
84
- <a name='L19'></a><a href='#L19'>19</a>
85
- <a name='L20'></a><a href='#L20'>20</a>
86
- <a name='L21'></a><a href='#L21'>21</a>
87
- <a name='L22'></a><a href='#L22'>22</a>
88
- <a name='L23'></a><a href='#L23'>23</a>
89
- <a name='L24'></a><a href='#L24'>24</a>
90
- <a name='L25'></a><a href='#L25'>25</a>
91
- <a name='L26'></a><a href='#L26'>26</a>
92
- <a name='L27'></a><a href='#L27'>27</a>
93
- <a name='L28'></a><a href='#L28'>28</a>
94
- <a name='L29'></a><a href='#L29'>29</a>
95
- <a name='L30'></a><a href='#L30'>30</a>
96
- <a name='L31'></a><a href='#L31'>31</a>
97
- <a name='L32'></a><a href='#L32'>32</a>
98
- <a name='L33'></a><a href='#L33'>33</a>
99
- <a name='L34'></a><a href='#L34'>34</a>
100
- <a name='L35'></a><a href='#L35'>35</a>
101
- <a name='L36'></a><a href='#L36'>36</a>
102
- <a name='L37'></a><a href='#L37'>37</a>
103
- <a name='L38'></a><a href='#L38'>38</a>
104
- <a name='L39'></a><a href='#L39'>39</a>
105
- <a name='L40'></a><a href='#L40'>40</a>
106
- <a name='L41'></a><a href='#L41'>41</a>
107
- <a name='L42'></a><a href='#L42'>42</a>
108
- <a name='L43'></a><a href='#L43'>43</a>
109
- <a name='L44'></a><a href='#L44'>44</a>
110
- <a name='L45'></a><a href='#L45'>45</a>
111
- <a name='L46'></a><a href='#L46'>46</a>
112
- <a name='L47'></a><a href='#L47'>47</a>
113
- <a name='L48'></a><a href='#L48'>48</a>
114
- <a name='L49'></a><a href='#L49'>49</a>
115
- <a name='L50'></a><a href='#L50'>50</a>
116
- <a name='L51'></a><a href='#L51'>51</a></td><td class="line-coverage quiet"><span class="cline-any cline-neutral">&nbsp;</span>
117
- <span class="cline-any cline-neutral">&nbsp;</span>
118
- <span class="cline-any cline-neutral">&nbsp;</span>
119
- <span class="cline-any cline-neutral">&nbsp;</span>
120
- <span class="cline-any cline-neutral">&nbsp;</span>
121
- <span class="cline-any cline-neutral">&nbsp;</span>
122
- <span class="cline-any cline-neutral">&nbsp;</span>
123
- <span class="cline-any cline-neutral">&nbsp;</span>
124
- <span class="cline-any cline-neutral">&nbsp;</span>
125
- <span class="cline-any cline-neutral">&nbsp;</span>
126
- <span class="cline-any cline-neutral">&nbsp;</span>
127
- <span class="cline-any cline-neutral">&nbsp;</span>
128
- <span class="cline-any cline-neutral">&nbsp;</span>
129
- <span class="cline-any cline-neutral">&nbsp;</span>
130
- <span class="cline-any cline-neutral">&nbsp;</span>
131
- <span class="cline-any cline-neutral">&nbsp;</span>
132
- <span class="cline-any cline-neutral">&nbsp;</span>
133
- <span class="cline-any cline-neutral">&nbsp;</span>
134
- <span class="cline-any cline-neutral">&nbsp;</span>
135
- <span class="cline-any cline-neutral">&nbsp;</span>
136
- <span class="cline-any cline-neutral">&nbsp;</span>
137
- <span class="cline-any cline-neutral">&nbsp;</span>
138
- <span class="cline-any cline-neutral">&nbsp;</span>
139
- <span class="cline-any cline-neutral">&nbsp;</span>
140
- <span class="cline-any cline-neutral">&nbsp;</span>
141
- <span class="cline-any cline-yes">12x</span>
142
- <span class="cline-any cline-neutral">&nbsp;</span>
143
- <span class="cline-any cline-neutral">&nbsp;</span>
144
- <span class="cline-any cline-yes">4x</span>
145
- <span class="cline-any cline-yes">4x</span>
146
- <span class="cline-any cline-neutral">&nbsp;</span>
147
- <span class="cline-any cline-neutral">&nbsp;</span>
148
- <span class="cline-any cline-neutral">&nbsp;</span>
149
- <span class="cline-any cline-neutral">&nbsp;</span>
150
- <span class="cline-any cline-neutral">&nbsp;</span>
151
- <span class="cline-any cline-neutral">&nbsp;</span>
152
- <span class="cline-any cline-neutral">&nbsp;</span>
153
- <span class="cline-any cline-neutral">&nbsp;</span>
154
- <span class="cline-any cline-neutral">&nbsp;</span>
155
- <span class="cline-any cline-yes">3x</span>
156
- <span class="cline-any cline-neutral">&nbsp;</span>
157
- <span class="cline-any cline-neutral">&nbsp;</span>
158
- <span class="cline-any cline-neutral">&nbsp;</span>
159
- <span class="cline-any cline-neutral">&nbsp;</span>
160
- <span class="cline-any cline-neutral">&nbsp;</span>
161
- <span class="cline-any cline-neutral">&nbsp;</span>
162
- <span class="cline-any cline-neutral">&nbsp;</span>
163
- <span class="cline-any cline-no">&nbsp;</span>
164
- <span class="cline-any cline-neutral">&nbsp;</span>
165
- <span class="cline-any cline-neutral">&nbsp;</span>
166
- <span class="cline-any cline-neutral">&nbsp;</span></td><td class="text"><pre class="prettyprint lang-js">/**
167
- * Zero-config default `SnapshotStore`. The storage MECHANICS (composite-key
168
- * map, never returning rows across tenants) are NOT re-implemented here —
169
- * they are composed from `@nebutra/tenant-store`'s `InMemoryTenantStore`,
170
- * which already enforces tenant isolation structurally via its
171
- * `tenantId:id` key plus a defense-in-depth tenantId equality check.
172
- *
173
- * A production deployment swaps this for a Prisma/Redis adapter that
174
- * implements the same `SnapshotStore` interface; the README documents that
175
- * shape. The isolation property a Prisma adapter would get from RLS is here
176
- * provided by the borrowed composite key.
177
- */
178
- &nbsp;
179
- import {
180
- InMemoryTenantStore,
181
- type TenantOwned,
182
- } from "@nebutra/tenant-store";
183
- import type { SnapshotStore } from "../types";
184
- &nbsp;
185
- interface SnapshotRow extends TenantOwned {
186
- readonly tenantId: string;
187
- readonly state: Uint8Array;
188
- }
189
- &nbsp;
190
- export class InMemorySnapshotStore implements SnapshotStore {
191
- private readonly inner = new InMemoryTenantStore&lt;SnapshotRow&gt;();
192
- &nbsp;
193
- async load(tenantId: string, roomId: string): Promise&lt;Uint8Array | null&gt; {
194
- const row = await this.inner.read(tenantId, roomId);
195
- return row ? row.state : null;
196
- }
197
- &nbsp;
198
- async save(
199
- tenantId: string,
200
- roomId: string,
201
- state: Uint8Array,
202
- ): Promise&lt;void&gt; {
203
- // Copy so a later in-place mutation of the caller's buffer can't
204
- // retroactively corrupt persisted state.
205
- await this.inner.write(tenantId, roomId, {
206
- tenantId,
207
- state: Uint8Array.from(state),
208
- });
209
- }
210
- &nbsp;
211
- /** Test helper. */
212
- <span class="fstat-no" title="function not covered" > clear(): void {</span>
213
- <span class="cstat-no" title="statement not covered" > this.inner.clear();</span>
214
- }
215
- }
216
- &nbsp;</pre></td></tr></table></pre>
217
-
218
- <div class='push'></div><!-- for sticky footer -->
219
- </div><!-- /wrapper -->
220
- <div class='footer quiet pad2 space-top1 center small'>
221
- Code coverage generated by
222
- <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
223
- at 2026-05-17T09:08:52.337Z
224
- </div>
225
- <script src="../../prettify.js"></script>
226
- <script>
227
- window.onload = function () {
228
- prettyPrint();
229
- };
230
- </script>
231
- <script src="../../sorter.js"></script>
232
- <script src="../../block-navigation.js"></script>
233
- </body>
234
- </html>
235
-
@@ -1,116 +0,0 @@
1
-
2
- <!doctype html>
3
- <html lang="en">
4
-
5
- <head>
6
- <title>Code coverage report for src/transport</title>
7
- <meta charset="utf-8" />
8
- <link rel="stylesheet" href="../../prettify.css" />
9
- <link rel="stylesheet" href="../../base.css" />
10
- <link rel="shortcut icon" type="image/x-icon" href="../../favicon.png" />
11
- <meta name="viewport" content="width=device-width, initial-scale=1" />
12
- <style type='text/css'>
13
- .coverage-summary .sorter {
14
- background-image: url(../../sort-arrow-sprite.png);
15
- }
16
- </style>
17
- </head>
18
-
19
- <body>
20
- <div class='wrapper'>
21
- <div class='pad1'>
22
- <h1><a href="../../index.html">All files</a> src/transport</h1>
23
- <div class='clearfix'>
24
-
25
- <div class='fl pad1y space-right2'>
26
- <span class="strong">90% </span>
27
- <span class="quiet">Statements</span>
28
- <span class='fraction'>18/20</span>
29
- </div>
30
-
31
-
32
- <div class='fl pad1y space-right2'>
33
- <span class="strong">75% </span>
34
- <span class="quiet">Branches</span>
35
- <span class='fraction'>6/8</span>
36
- </div>
37
-
38
-
39
- <div class='fl pad1y space-right2'>
40
- <span class="strong">100% </span>
41
- <span class="quiet">Functions</span>
42
- <span class='fraction'>4/4</span>
43
- </div>
44
-
45
-
46
- <div class='fl pad1y space-right2'>
47
- <span class="strong">100% </span>
48
- <span class="quiet">Lines</span>
49
- <span class='fraction'>16/16</span>
50
- </div>
51
-
52
-
53
- </div>
54
- <p class="quiet">
55
- Press <em>n</em> or <em>j</em> to go to the next uncovered block, <em>b</em>, <em>p</em> or <em>k</em> for the previous block.
56
- </p>
57
- <template id="filterTemplate">
58
- <div class="quiet">
59
- Filter:
60
- <input type="search" id="fileSearch">
61
- </div>
62
- </template>
63
- </div>
64
- <div class='status-line high'></div>
65
- <div class="pad1">
66
- <table class="coverage-summary">
67
- <thead>
68
- <tr>
69
- <th data-col="file" data-fmt="html" data-html="true" class="file">File</th>
70
- <th data-col="pic" data-type="number" data-fmt="html" data-html="true" class="pic"></th>
71
- <th data-col="statements" data-type="number" data-fmt="pct" class="pct">Statements</th>
72
- <th data-col="statements_raw" data-type="number" data-fmt="html" class="abs"></th>
73
- <th data-col="branches" data-type="number" data-fmt="pct" class="pct">Branches</th>
74
- <th data-col="branches_raw" data-type="number" data-fmt="html" class="abs"></th>
75
- <th data-col="functions" data-type="number" data-fmt="pct" class="pct">Functions</th>
76
- <th data-col="functions_raw" data-type="number" data-fmt="html" class="abs"></th>
77
- <th data-col="lines" data-type="number" data-fmt="pct" class="pct">Lines</th>
78
- <th data-col="lines_raw" data-type="number" data-fmt="html" class="abs"></th>
79
- </tr>
80
- </thead>
81
- <tbody><tr>
82
- <td class="file high" data-value="loopback.ts"><a href="loopback.ts.html">loopback.ts</a></td>
83
- <td data-value="90" class="pic high">
84
- <div class="chart"><div class="cover-fill" style="width: 90%"></div><div class="cover-empty" style="width: 10%"></div></div>
85
- </td>
86
- <td data-value="90" class="pct high">90%</td>
87
- <td data-value="20" class="abs high">18/20</td>
88
- <td data-value="75" class="pct medium">75%</td>
89
- <td data-value="8" class="abs medium">6/8</td>
90
- <td data-value="100" class="pct high">100%</td>
91
- <td data-value="4" class="abs high">4/4</td>
92
- <td data-value="100" class="pct high">100%</td>
93
- <td data-value="16" class="abs high">16/16</td>
94
- </tr>
95
-
96
- </tbody>
97
- </table>
98
- </div>
99
- <div class='push'></div><!-- for sticky footer -->
100
- </div><!-- /wrapper -->
101
- <div class='footer quiet pad2 space-top1 center small'>
102
- Code coverage generated by
103
- <a href="https://istanbul.js.org/" target="_blank" rel="noopener noreferrer">istanbul</a>
104
- at 2026-05-17T09:08:52.337Z
105
- </div>
106
- <script src="../../prettify.js"></script>
107
- <script>
108
- window.onload = function () {
109
- prettyPrint();
110
- };
111
- </script>
112
- <script src="../../sorter.js"></script>
113
- <script src="../../block-navigation.js"></script>
114
- </body>
115
- </html>
116
-
@@ -1,39 +0,0 @@
1
- /**
2
- * Example 3 — snapshot + restore across hub lifetimes.
3
- *
4
- * Persist a room via the injected SnapshotStore, then hydrate a brand-new
5
- * hub from the same store. Demonstrates the persist-then-reload contract
6
- * used by long-lived documents/canvases.
7
- *
8
- * Run: pnpm --filter @nebutra/collab exec tsx examples/snapshot-restore.ts
9
- */
10
-
11
- import { createCollab } from "../src/index";
12
- import { InMemorySnapshotStore } from "../src/store/memory";
13
-
14
- export async function main(): Promise<void> {
15
- // A shared store stands in for Prisma/Redis in production.
16
- const store = new InMemorySnapshotStore();
17
-
18
- const hub1 = createCollab({ store });
19
- const r1 = hub1.room("t1", "doc-1");
20
- r1.doc.getText("body").insert(0, "persisted across restarts");
21
- await r1.snapshot();
22
- hub1.destroy();
23
-
24
- // New process / new hub, same store.
25
- const hub2 = createCollab({ store });
26
- const r2 = await hub2.roomRestored("t1", "doc-1");
27
- const restored = r2.doc.getText("body").toString();
28
- if (restored !== "persisted across restarts") {
29
- throw new Error(`restore failed, got: "${restored}"`);
30
- }
31
-
32
- // eslint-disable-next-line no-console
33
- console.warn(`[collab] restored after reload: "${restored}"`);
34
- hub2.destroy();
35
- }
36
-
37
- if (import.meta.url === `file://${process.argv[1]}`) {
38
- void main();
39
- }
@@ -1,42 +0,0 @@
1
- /**
2
- * Example 2 — tenant isolation (security-critical).
3
- *
4
- * Two tenants use the SAME roomId. Their docs, snapshot state and transport
5
- * channels are hard-partitioned: tenant A's edits are unreachable from
6
- * tenant B even though the room id string is identical.
7
- *
8
- * Run: pnpm --filter @nebutra/collab exec tsx examples/tenant-isolation.ts
9
- */
10
-
11
- import { getCollab } from "../src/index";
12
-
13
- export async function main(): Promise<void> {
14
- const hub = await getCollab();
15
-
16
- const roomId = "shared-room-id";
17
- const a = hub.room("tenantA", roomId);
18
- const b = hub.room("tenantB", roomId);
19
-
20
- if (a.doc === b.doc) throw new Error("tenant partition breached: shared doc");
21
-
22
- const leaked: unknown[] = [];
23
- b.onUpdate((u) => leaked.push(u));
24
-
25
- a.doc.getMap("m").set("secret", "A-only");
26
- await a.snapshot();
27
-
28
- if (b.doc.getMap("m").get("secret") !== undefined) {
29
- throw new Error("tenant partition breached: B saw A's value");
30
- }
31
- if (leaked.length !== 0) {
32
- throw new Error("tenant partition breached: B received A's update");
33
- }
34
-
35
- // eslint-disable-next-line no-console
36
- console.warn("[collab] tenant isolation holds: B never observed A");
37
- hub.destroy();
38
- }
39
-
40
- if (import.meta.url === `file://${process.argv[1]}`) {
41
- void main();
42
- }
@@ -1,43 +0,0 @@
1
- /**
2
- * Example 1 — zero-config CRDT convergence.
3
- *
4
- * No env, no config, real (non-mock) Yjs behaviour. This file is also the
5
- * in-package REAL CALLER of the exported `getCollab` factory that keeps
6
- * `@nebutra/collab` in the `active` tier of the three-tier module lifecycle.
7
- *
8
- * Run: pnpm --filter @nebutra/collab exec tsx examples/zero-config-convergence.ts
9
- */
10
-
11
- import * as Y from "yjs";
12
- import { getCollab } from "../src/index";
13
-
14
- export async function main(): Promise<void> {
15
- const hub = await getCollab();
16
-
17
- // Two clients on the SAME tenant + room.
18
- const a = hub.room("t1", "doc-1");
19
- const b = hub.room("t1", "doc-1");
20
- if (a !== b) throw new Error("same (tenant,room) must yield same instance");
21
-
22
- // A standalone peer doc simulating a second device.
23
- const peer = new Y.Doc();
24
- a.doc.getText("body").insert(0, "Hello ");
25
- Y.applyUpdate(peer, a.encodeState());
26
-
27
- peer.getText("body").insert(6, "collab");
28
- a.applyUpdate(Y.encodeStateAsUpdate(peer));
29
- Y.applyUpdate(peer, a.encodeState());
30
-
31
- const converged = a.doc.getText("body").toString();
32
- if (converged !== peer.getText("body").toString()) {
33
- throw new Error("clients did not converge");
34
- }
35
-
36
- // eslint-disable-next-line no-console
37
- console.warn(`[collab] converged text: "${converged}"`);
38
- hub.destroy();
39
- }
40
-
41
- if (import.meta.url === `file://${process.argv[1]}`) {
42
- void main();
43
- }