@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.
- package/dist/index.js +3 -3
- package/package.json +16 -8
- package/.turbo/turbo-build.log +0 -28
- package/.turbo/turbo-test.log +0 -14
- package/.turbo/turbo-typecheck.log +0 -4
- package/coverage/base.css +0 -224
- package/coverage/block-navigation.js +0 -87
- package/coverage/clover.xml +0 -156
- package/coverage/coverage-final.json +0 -7
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +0 -146
- package/coverage/prettify.css +0 -1
- package/coverage/prettify.js +0 -2
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +0 -210
- package/coverage/src/errors.ts.html +0 -211
- package/coverage/src/hub.ts.html +0 -619
- package/coverage/src/index.html +0 -161
- package/coverage/src/index.ts.html +0 -211
- package/coverage/src/room.ts.html +0 -529
- package/coverage/src/store/index.html +0 -116
- package/coverage/src/store/memory.ts.html +0 -235
- package/coverage/src/transport/index.html +0 -116
- package/coverage/src/transport/loopback.ts.html +0 -0
- package/examples/snapshot-restore.ts +0 -39
- package/examples/tenant-isolation.ts +0 -42
- package/examples/zero-config-convergence.ts +0 -43
- package/src/__tests__/collab.test.ts +0 -240
- package/src/cli.ts +0 -27
- package/src/errors.ts +0 -42
- package/src/hub.ts +0 -0
- package/src/index.ts +0 -42
- package/src/room.ts +0 -130
- package/src/store/memory.ts +0 -43
- package/src/transport/loopback.ts +0 -0
- package/src/types.ts +0 -64
- package/tsconfig.json +0 -12
- package/tsup.config.ts +0 -11
|
@@ -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"> </span>
|
|
117
|
-
<span class="cline-any cline-neutral"> </span>
|
|
118
|
-
<span class="cline-any cline-neutral"> </span>
|
|
119
|
-
<span class="cline-any cline-neutral"> </span>
|
|
120
|
-
<span class="cline-any cline-neutral"> </span>
|
|
121
|
-
<span class="cline-any cline-neutral"> </span>
|
|
122
|
-
<span class="cline-any cline-neutral"> </span>
|
|
123
|
-
<span class="cline-any cline-neutral"> </span>
|
|
124
|
-
<span class="cline-any cline-neutral"> </span>
|
|
125
|
-
<span class="cline-any cline-neutral"> </span>
|
|
126
|
-
<span class="cline-any cline-neutral"> </span>
|
|
127
|
-
<span class="cline-any cline-neutral"> </span>
|
|
128
|
-
<span class="cline-any cline-neutral"> </span>
|
|
129
|
-
<span class="cline-any cline-neutral"> </span>
|
|
130
|
-
<span class="cline-any cline-neutral"> </span>
|
|
131
|
-
<span class="cline-any cline-neutral"> </span>
|
|
132
|
-
<span class="cline-any cline-neutral"> </span>
|
|
133
|
-
<span class="cline-any cline-neutral"> </span>
|
|
134
|
-
<span class="cline-any cline-neutral"> </span>
|
|
135
|
-
<span class="cline-any cline-neutral"> </span>
|
|
136
|
-
<span class="cline-any cline-neutral"> </span>
|
|
137
|
-
<span class="cline-any cline-neutral"> </span>
|
|
138
|
-
<span class="cline-any cline-neutral"> </span>
|
|
139
|
-
<span class="cline-any cline-neutral"> </span>
|
|
140
|
-
<span class="cline-any cline-neutral"> </span>
|
|
141
|
-
<span class="cline-any cline-yes">12x</span>
|
|
142
|
-
<span class="cline-any cline-neutral"> </span>
|
|
143
|
-
<span class="cline-any cline-neutral"> </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"> </span>
|
|
147
|
-
<span class="cline-any cline-neutral"> </span>
|
|
148
|
-
<span class="cline-any cline-neutral"> </span>
|
|
149
|
-
<span class="cline-any cline-neutral"> </span>
|
|
150
|
-
<span class="cline-any cline-neutral"> </span>
|
|
151
|
-
<span class="cline-any cline-neutral"> </span>
|
|
152
|
-
<span class="cline-any cline-neutral"> </span>
|
|
153
|
-
<span class="cline-any cline-neutral"> </span>
|
|
154
|
-
<span class="cline-any cline-neutral"> </span>
|
|
155
|
-
<span class="cline-any cline-yes">3x</span>
|
|
156
|
-
<span class="cline-any cline-neutral"> </span>
|
|
157
|
-
<span class="cline-any cline-neutral"> </span>
|
|
158
|
-
<span class="cline-any cline-neutral"> </span>
|
|
159
|
-
<span class="cline-any cline-neutral"> </span>
|
|
160
|
-
<span class="cline-any cline-neutral"> </span>
|
|
161
|
-
<span class="cline-any cline-neutral"> </span>
|
|
162
|
-
<span class="cline-any cline-neutral"> </span>
|
|
163
|
-
<span class="cline-any cline-no"> </span>
|
|
164
|
-
<span class="cline-any cline-neutral"> </span>
|
|
165
|
-
<span class="cline-any cline-neutral"> </span>
|
|
166
|
-
<span class="cline-any cline-neutral"> </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
|
-
|
|
179
|
-
import {
|
|
180
|
-
InMemoryTenantStore,
|
|
181
|
-
type TenantOwned,
|
|
182
|
-
} from "@nebutra/tenant-store";
|
|
183
|
-
import type { SnapshotStore } from "../types";
|
|
184
|
-
|
|
185
|
-
interface SnapshotRow extends TenantOwned {
|
|
186
|
-
readonly tenantId: string;
|
|
187
|
-
readonly state: Uint8Array;
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
export class InMemorySnapshotStore implements SnapshotStore {
|
|
191
|
-
private readonly inner = new InMemoryTenantStore<SnapshotRow>();
|
|
192
|
-
|
|
193
|
-
async load(tenantId: string, roomId: string): Promise<Uint8Array | null> {
|
|
194
|
-
const row = await this.inner.read(tenantId, roomId);
|
|
195
|
-
return row ? row.state : null;
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
async save(
|
|
199
|
-
tenantId: string,
|
|
200
|
-
roomId: string,
|
|
201
|
-
state: Uint8Array,
|
|
202
|
-
): Promise<void> {
|
|
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
|
-
|
|
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
|
-
</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
|
-
|
|
Binary file
|
|
@@ -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
|
-
}
|