@nebutra/collab 0.2.0 → 0.2.2
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/.turbo/turbo-build.log +25 -25
- package/.turbo/turbo-test.log +7 -7
- package/.turbo/turbo-typecheck.log +1 -1
- package/CHANGELOG.md +34 -2
- package/LICENSE +21 -676
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +156 -0
- package/coverage/coverage-final.json +7 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +146 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/errors.ts.html +211 -0
- package/coverage/src/hub.ts.html +619 -0
- package/coverage/src/index.html +161 -0
- package/coverage/src/index.ts.html +211 -0
- package/coverage/src/room.ts.html +529 -0
- package/coverage/src/store/index.html +116 -0
- package/coverage/src/store/memory.ts.html +235 -0
- package/coverage/src/transport/index.html +116 -0
- package/coverage/src/transport/loopback.ts.html +0 -0
- package/dist/index.js +25 -21
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/src/hub.ts +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
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
|
+
|
|
@@ -0,0 +1,116 @@
|
|
|
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
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
InMemorySnapshotStore
|
|
3
|
-
} from "./chunk-6XHNO2RP.js";
|
|
4
1
|
import {
|
|
5
2
|
LoopbackTransport
|
|
6
3
|
} from "./chunk-S2HTNEZN.js";
|
|
4
|
+
import {
|
|
5
|
+
InMemorySnapshotStore
|
|
6
|
+
} from "./chunk-6XHNO2RP.js";
|
|
7
7
|
|
|
8
8
|
// src/errors.ts
|
|
9
9
|
import { CapabilityError } from "@nebutra/capability-kit";
|
|
@@ -41,6 +41,10 @@ var Room = class {
|
|
|
41
41
|
Y.applyUpdate(this.doc, update, REMOTE_ORIGIN);
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
|
+
tenantId;
|
|
45
|
+
roomId;
|
|
46
|
+
store;
|
|
47
|
+
transport;
|
|
44
48
|
doc;
|
|
45
49
|
listeners = /* @__PURE__ */ new Set();
|
|
46
50
|
unsubTransport;
|
|
@@ -148,7 +152,8 @@ var Hub = class {
|
|
|
148
152
|
}
|
|
149
153
|
async doctor() {
|
|
150
154
|
const start = Date.now();
|
|
151
|
-
let
|
|
155
|
+
let yjsOk = false;
|
|
156
|
+
let yjsDetail = "";
|
|
152
157
|
try {
|
|
153
158
|
const Y2 = await import("yjs");
|
|
154
159
|
const probe = new Y2.Doc();
|
|
@@ -158,27 +163,27 @@ var Hub = class {
|
|
|
158
163
|
const ok = restored.getMap("p").get("k") === 1;
|
|
159
164
|
probe.destroy();
|
|
160
165
|
restored.destroy();
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
detail: ok ? "Yjs encode/apply round-trip succeeded." : "Yjs round-trip produced unexpected state."
|
|
164
|
-
};
|
|
166
|
+
yjsOk = ok;
|
|
167
|
+
yjsDetail = ok ? "Yjs encode/apply round-trip succeeded." : "Yjs round-trip produced unexpected state.";
|
|
165
168
|
} catch (e) {
|
|
166
|
-
|
|
169
|
+
yjsDetail = `Yjs unavailable: ${String(e)}`;
|
|
167
170
|
}
|
|
168
|
-
|
|
171
|
+
const yjs = { ok: yjsOk, detail: yjsDetail };
|
|
172
|
+
let storeOk = false;
|
|
173
|
+
let storeDetail = "";
|
|
169
174
|
try {
|
|
170
175
|
const probe = new Uint8Array([7, 7, 7]);
|
|
171
176
|
await this.store.save("__collab_doctor__", "__probe__", probe);
|
|
172
177
|
const back = await this.store.load("__collab_doctor__", "__probe__");
|
|
173
178
|
const ok = !!back && back.length === probe.length && back[0] === 7;
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
detail: ok ? "SnapshotStore save/load round-trip succeeded." : "SnapshotStore returned unexpected bytes."
|
|
177
|
-
};
|
|
179
|
+
storeOk = ok;
|
|
180
|
+
storeDetail = ok ? "SnapshotStore save/load round-trip succeeded." : "SnapshotStore returned unexpected bytes.";
|
|
178
181
|
} catch (e) {
|
|
179
|
-
|
|
182
|
+
storeDetail = `SnapshotStore error: ${String(e)}`;
|
|
180
183
|
}
|
|
181
|
-
|
|
184
|
+
const store = { ok: storeOk, detail: storeDetail };
|
|
185
|
+
let transportOk = false;
|
|
186
|
+
let transportDetail = "";
|
|
182
187
|
try {
|
|
183
188
|
let received = false;
|
|
184
189
|
const off = this.transport.subscribe("__collab_doctor__", "__probe__", () => {
|
|
@@ -186,13 +191,12 @@ var Hub = class {
|
|
|
186
191
|
});
|
|
187
192
|
await this.transport.broadcast("__collab_doctor__", "__probe__", new Uint8Array([1]));
|
|
188
193
|
off();
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
detail: received ? "Transport broadcast/subscribe round-trip succeeded." : "Transport did not deliver the probe (network adapter may be async)."
|
|
192
|
-
};
|
|
194
|
+
transportOk = received;
|
|
195
|
+
transportDetail = received ? "Transport broadcast/subscribe round-trip succeeded." : "Transport did not deliver the probe (network adapter may be async).";
|
|
193
196
|
} catch (e) {
|
|
194
|
-
|
|
197
|
+
transportDetail = `Transport error: ${String(e)}`;
|
|
195
198
|
}
|
|
199
|
+
const transport = { ok: transportOk, detail: transportDetail };
|
|
196
200
|
return {
|
|
197
201
|
ok: yjs.ok && store.ok && transport.ok,
|
|
198
202
|
durationMs: Date.now() - start,
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/errors.ts","../src/room.ts","../src/hub.ts"],"sourcesContent":["/**\n * Every failure surfaced by this package is a `CollabError`. The contract is\n * deliberately strict: a machine-stable `code` and a human-actionable\n * `suggestion` are MANDATORY, so no code path can throw a bare `Error` that\n * leaves a caller without a remediation hint.\n *\n * Mechanics (code/suggestion/toJSON/empty-suggestion fallback) are inherited\n * from the shared `@nebutra/capability-kit` `CapabilityError`; this subclass\n * only pins collab's error name + its package-specific fallback wording, so\n * the observable contract is unchanged.\n */\n\nimport { CapabilityError } from \"@nebutra/capability-kit\";\n\nexport type CollabErrorCode =\n | \"COLLAB_INVALID_TENANT\"\n | \"COLLAB_INVALID_ROOM\"\n | \"COLLAB_SNAPSHOT_FAILED\"\n | \"COLLAB_RESTORE_FAILED\"\n | \"COLLAB_DESTROYED\"\n | \"COLLAB_TEST\"\n | (string & {});\n\nexport interface CollabErrorInit {\n readonly code: CollabErrorCode;\n /** A non-empty, actionable remediation hint. */\n readonly suggestion: string;\n readonly cause?: unknown;\n}\n\nexport class CollabError extends CapabilityError {\n declare readonly code: CollabErrorCode;\n\n constructor(message: string, init: CollabErrorInit) {\n super(message, init, {\n name: \"CollabError\",\n emptySuggestionFallback:\n \"No suggestion was provided. This is a bug in @nebutra/collab — \" +\n \"report it with the failing operation.\",\n });\n }\n}\n","/**\n * A single tenant-scoped CRDT room. One `Y.Doc` per (tenant, room). The hub\n * owns the partitioning; this class assumes its `tenantId`/`roomId` are\n * already the partition it belongs to and never reaches outside them.\n *\n * Snapshot persistence is serialized through `withTenantLock(tenantId,\n * roomId, ...)` borrowed from `@nebutra/tenant-store` rather than a\n * hand-rolled mutex — same primitive used by canvas/reel, so a future swap\n * to a distributed lock changes one place.\n */\n\nimport { withTenantLock } from \"@nebutra/tenant-store\";\nimport * as Y from \"yjs\";\nimport { CollabError } from \"./errors\";\nimport type { CollabRoom, CollabTransport, SnapshotStore, UpdateListener } from \"./types\";\n\n/** Origin tag used when applying remote updates so we don't echo them back. */\nconst REMOTE_ORIGIN = Symbol(\"collab.remote\");\n\nexport class Room implements CollabRoom {\n readonly doc: Y.Doc;\n private readonly listeners = new Set<UpdateListener>();\n private readonly unsubTransport: () => void;\n private destroyed = false;\n\n constructor(\n readonly tenantId: string,\n readonly roomId: string,\n private readonly store: SnapshotStore,\n private readonly transport: CollabTransport,\n ) {\n this.doc = new Y.Doc();\n\n // Fan local updates out to: registered listeners + the transport. The\n // transport echo is guarded by origin so a remote-applied update is not\n // re-broadcast into a loop.\n this.doc.on(\"update\", (update: Uint8Array, origin: unknown) => {\n for (const cb of [...this.listeners]) cb(update, origin);\n if (origin !== REMOTE_ORIGIN) {\n void Promise.resolve(this.transport.broadcast(this.tenantId, this.roomId, update)).catch(\n () => {\n // Transport delivery is best-effort; CRDT state stays correct and\n // converges on the next exchanged update. Swallowing here avoids\n // an unhandled rejection from a flaky network adapter.\n },\n );\n }\n });\n\n // Remote updates for THIS tenant-scoped channel only.\n this.unsubTransport = this.transport.subscribe(this.tenantId, this.roomId, (update) => {\n if (this.destroyed) return;\n Y.applyUpdate(this.doc, update, REMOTE_ORIGIN);\n });\n }\n\n applyUpdate(update: Uint8Array, origin?: unknown): void {\n this.assertLive();\n Y.applyUpdate(this.doc, update, origin);\n }\n\n encodeState(): Uint8Array {\n this.assertLive();\n return Y.encodeStateAsUpdate(this.doc);\n }\n\n onUpdate(cb: UpdateListener): () => void {\n this.assertLive();\n this.listeners.add(cb);\n return () => {\n this.listeners.delete(cb);\n };\n }\n\n async snapshot(): Promise<void> {\n this.assertLive();\n const state = this.encodeState();\n try {\n // Serialize concurrent snapshots of the SAME room; different rooms (or\n // the same room under another tenant) persist in parallel.\n await withTenantLock(this.tenantId, this.roomId, () =>\n this.store.save(this.tenantId, this.roomId, state),\n );\n } catch (cause) {\n throw new CollabError(`Failed to persist snapshot for room \"${this.roomId}\".`, {\n code: \"COLLAB_SNAPSHOT_FAILED\",\n suggestion:\n \"Verify the configured SnapshotStore is reachable (DB/Redis up, \" +\n \"credentials valid). The in-memory default never fails; a custom \" +\n \"adapter likely threw.\",\n cause,\n });\n }\n }\n\n /** Hydrate this doc from persisted state, if any. Internal to the hub. */\n async _restore(): Promise<void> {\n try {\n const persisted = await this.store.load(this.tenantId, this.roomId);\n if (persisted) Y.applyUpdate(this.doc, persisted, REMOTE_ORIGIN);\n } catch (cause) {\n throw new CollabError(`Failed to restore room \"${this.roomId}\" from snapshot store.`, {\n code: \"COLLAB_RESTORE_FAILED\",\n suggestion:\n \"Check the SnapshotStore adapter's load() — it should resolve \" +\n \"null (not throw) when no snapshot exists for the tenant+room.\",\n cause,\n });\n }\n }\n\n destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.unsubTransport();\n this.listeners.clear();\n this.doc.destroy();\n }\n\n private assertLive(): void {\n if (this.destroyed) {\n throw new CollabError(`Room \"${this.roomId}\" was destroyed and can no longer be used.`, {\n code: \"COLLAB_DESTROYED\",\n suggestion:\n \"Acquire a fresh room via hub.room(tenantId, roomId) instead of \" +\n \"reusing a destroyed instance.\",\n });\n }\n }\n}\n","/**\n * `CollabHub` — the tenant-partition boundary.\n *\n * TENANT ISOLATION IS STRUCTURAL: rooms are stored in a `Map` keyed by a\n * composite `tenantId\u0000roomId` string with a NUL separator that cannot\n * appear in a normal id. There is no API that takes only a roomId, so a key\n * minted for tenant A is unreachable from tenant B — a room handle is only\n * ever produced by passing an explicit `tenantId`, and the snapshot store +\n * transport are likewise addressed by (tenantId, roomId). The same property\n * a Prisma adapter gets from RLS is enforced here by the composite key, with\n * no trust placed in caller-supplied payload.\n */\n\nimport { CollabError } from \"./errors\";\nimport { Room } from \"./room\";\nimport { InMemorySnapshotStore } from \"./store/memory\";\nimport { LoopbackTransport } from \"./transport/loopback\";\nimport type {\n CollabConfig,\n CollabHub,\n CollabRoom,\n CollabTransport,\n DoctorReport,\n SnapshotStore,\n} from \"./types\";\n\n// NUL separator: it cannot appear in a normal tenant/room id, so the pairs\n// (\"a\",\"bc\") and (\"ab\",\"c\") can never collide into the same room key — the\n// tenant partition is exact, not merely conventional.\nconst KEY_SEP = \"\u0000\";\n\nfunction roomKey(tenantId: string, roomId: string): string {\n return `${tenantId}${KEY_SEP}${roomId}`;\n}\n\nfunction assertId(value: string, kind: \"tenant\" | \"room\"): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new CollabError(`A non-empty ${kind}Id is required.`, {\n code: kind === \"tenant\" ? \"COLLAB_INVALID_TENANT\" : \"COLLAB_INVALID_ROOM\",\n suggestion:\n kind === \"tenant\"\n ? \"Pass the current tenant id, e.g. from getCurrentTenant().tenantId — \" +\n \"never call room() without an explicit tenant.\"\n : \"Pass a stable, non-empty roomId (document/canvas id).\",\n });\n }\n}\n\nclass Hub implements CollabHub {\n private readonly rooms = new Map<string, Room>();\n private readonly store: SnapshotStore;\n private readonly transport: CollabTransport;\n\n constructor(config: CollabConfig = {}) {\n this.store = config.store ?? new InMemorySnapshotStore();\n this.transport = config.transport ?? new LoopbackTransport();\n }\n\n room(tenantId: string, roomId: string): CollabRoom {\n assertId(tenantId, \"tenant\");\n assertId(roomId, \"room\");\n const key = roomKey(tenantId, roomId);\n let room = this.rooms.get(key);\n if (!room) {\n room = new Room(tenantId, roomId, this.store, this.transport);\n this.rooms.set(key, room);\n }\n return room;\n }\n\n async roomRestored(tenantId: string, roomId: string): Promise<CollabRoom> {\n const key = roomKey(tenantId, roomId);\n const existed = this.rooms.has(key);\n const room = this.room(tenantId, roomId) as Room;\n // Only hydrate a freshly created room; an already-live room is the\n // authoritative in-memory state.\n if (!existed) await room._restore();\n return room;\n }\n\n async doctor(): Promise<DoctorReport> {\n const start = Date.now();\n\n // 1. Yjs presence + a real round-trip (not just \"is it imported\").\n let yjs = { ok: false, detail: \"\" };\n try {\n const Y = await import(\"yjs\");\n const probe = new Y.Doc();\n probe.getMap(\"p\").set(\"k\", 1);\n const restored = new Y.Doc();\n Y.applyUpdate(restored, Y.encodeStateAsUpdate(probe));\n const ok = restored.getMap(\"p\").get(\"k\") === 1;\n probe.destroy();\n restored.destroy();\n yjs = {\n ok,\n detail: ok\n ? \"Yjs encode/apply round-trip succeeded.\"\n : \"Yjs round-trip produced unexpected state.\",\n };\n } catch (e) {\n yjs = { ok: false, detail: `Yjs unavailable: ${String(e)}` };\n }\n\n // 2. Store health — write+read under a reserved diagnostic tenant.\n let store = { ok: false, detail: \"\" };\n try {\n const probe = new Uint8Array([7, 7, 7]);\n await this.store.save(\"__collab_doctor__\", \"__probe__\", probe);\n const back = await this.store.load(\"__collab_doctor__\", \"__probe__\");\n const ok = !!back && back.length === probe.length && back[0] === 7;\n store = {\n ok,\n detail: ok\n ? \"SnapshotStore save/load round-trip succeeded.\"\n : \"SnapshotStore returned unexpected bytes.\",\n };\n } catch (e) {\n store = { ok: false, detail: `SnapshotStore error: ${String(e)}` };\n }\n\n // 3. Transport health — subscribe, broadcast, observe, unsubscribe.\n let transport = { ok: false, detail: \"\" };\n try {\n let received = false;\n const off = this.transport.subscribe(\"__collab_doctor__\", \"__probe__\", () => {\n received = true;\n });\n await this.transport.broadcast(\"__collab_doctor__\", \"__probe__\", new Uint8Array([1]));\n off();\n transport = {\n ok: received,\n detail: received\n ? \"Transport broadcast/subscribe round-trip succeeded.\"\n : \"Transport did not deliver the probe (network adapter may be async).\",\n };\n } catch (e) {\n transport = { ok: false, detail: `Transport error: ${String(e)}` };\n }\n\n return {\n ok: yjs.ok && store.ok && transport.ok,\n durationMs: Date.now() - start,\n checks: { yjs, store, transport },\n };\n }\n\n destroy(): void {\n for (const room of this.rooms.values()) room.destroy();\n this.rooms.clear();\n }\n}\n\n/** Synchronous factory. */\nexport function createCollab(config?: CollabConfig): CollabHub {\n return new Hub(config);\n}\n\n/**\n * Async factory mirroring other Sailor integration packages. There is no\n * env/credential negotiation for the zero-config defaults, so this resolves\n * immediately — the async shape is reserved for future credentialed\n * store/transport providers without a breaking signature change.\n */\nexport async function getCollab(config?: CollabConfig): Promise<CollabHub> {\n return createCollab(config);\n}\n"],"mappings":"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","names":["Y"]}
|
|
1
|
+
{"version":3,"sources":["../src/errors.ts","../src/room.ts","../src/hub.ts"],"sourcesContent":["/**\n * Every failure surfaced by this package is a `CollabError`. The contract is\n * deliberately strict: a machine-stable `code` and a human-actionable\n * `suggestion` are MANDATORY, so no code path can throw a bare `Error` that\n * leaves a caller without a remediation hint.\n *\n * Mechanics (code/suggestion/toJSON/empty-suggestion fallback) are inherited\n * from the shared `@nebutra/capability-kit` `CapabilityError`; this subclass\n * only pins collab's error name + its package-specific fallback wording, so\n * the observable contract is unchanged.\n */\n\nimport { CapabilityError } from \"@nebutra/capability-kit\";\n\nexport type CollabErrorCode =\n | \"COLLAB_INVALID_TENANT\"\n | \"COLLAB_INVALID_ROOM\"\n | \"COLLAB_SNAPSHOT_FAILED\"\n | \"COLLAB_RESTORE_FAILED\"\n | \"COLLAB_DESTROYED\"\n | \"COLLAB_TEST\"\n | (string & {});\n\nexport interface CollabErrorInit {\n readonly code: CollabErrorCode;\n /** A non-empty, actionable remediation hint. */\n readonly suggestion: string;\n readonly cause?: unknown;\n}\n\nexport class CollabError extends CapabilityError {\n declare readonly code: CollabErrorCode;\n\n constructor(message: string, init: CollabErrorInit) {\n super(message, init, {\n name: \"CollabError\",\n emptySuggestionFallback:\n \"No suggestion was provided. This is a bug in @nebutra/collab — \" +\n \"report it with the failing operation.\",\n });\n }\n}\n","/**\n * A single tenant-scoped CRDT room. One `Y.Doc` per (tenant, room). The hub\n * owns the partitioning; this class assumes its `tenantId`/`roomId` are\n * already the partition it belongs to and never reaches outside them.\n *\n * Snapshot persistence is serialized through `withTenantLock(tenantId,\n * roomId, ...)` borrowed from `@nebutra/tenant-store` rather than a\n * hand-rolled mutex — same primitive used by canvas/reel, so a future swap\n * to a distributed lock changes one place.\n */\n\nimport { withTenantLock } from \"@nebutra/tenant-store\";\nimport * as Y from \"yjs\";\nimport { CollabError } from \"./errors\";\nimport type { CollabRoom, CollabTransport, SnapshotStore, UpdateListener } from \"./types\";\n\n/** Origin tag used when applying remote updates so we don't echo them back. */\nconst REMOTE_ORIGIN = Symbol(\"collab.remote\");\n\nexport class Room implements CollabRoom {\n readonly doc: Y.Doc;\n private readonly listeners = new Set<UpdateListener>();\n private readonly unsubTransport: () => void;\n private destroyed = false;\n\n constructor(\n readonly tenantId: string,\n readonly roomId: string,\n private readonly store: SnapshotStore,\n private readonly transport: CollabTransport,\n ) {\n this.doc = new Y.Doc();\n\n // Fan local updates out to: registered listeners + the transport. The\n // transport echo is guarded by origin so a remote-applied update is not\n // re-broadcast into a loop.\n this.doc.on(\"update\", (update: Uint8Array, origin: unknown) => {\n for (const cb of [...this.listeners]) cb(update, origin);\n if (origin !== REMOTE_ORIGIN) {\n void Promise.resolve(this.transport.broadcast(this.tenantId, this.roomId, update)).catch(\n () => {\n // Transport delivery is best-effort; CRDT state stays correct and\n // converges on the next exchanged update. Swallowing here avoids\n // an unhandled rejection from a flaky network adapter.\n },\n );\n }\n });\n\n // Remote updates for THIS tenant-scoped channel only.\n this.unsubTransport = this.transport.subscribe(this.tenantId, this.roomId, (update) => {\n if (this.destroyed) return;\n Y.applyUpdate(this.doc, update, REMOTE_ORIGIN);\n });\n }\n\n applyUpdate(update: Uint8Array, origin?: unknown): void {\n this.assertLive();\n Y.applyUpdate(this.doc, update, origin);\n }\n\n encodeState(): Uint8Array {\n this.assertLive();\n return Y.encodeStateAsUpdate(this.doc);\n }\n\n onUpdate(cb: UpdateListener): () => void {\n this.assertLive();\n this.listeners.add(cb);\n return () => {\n this.listeners.delete(cb);\n };\n }\n\n async snapshot(): Promise<void> {\n this.assertLive();\n const state = this.encodeState();\n try {\n // Serialize concurrent snapshots of the SAME room; different rooms (or\n // the same room under another tenant) persist in parallel.\n await withTenantLock(this.tenantId, this.roomId, () =>\n this.store.save(this.tenantId, this.roomId, state),\n );\n } catch (cause) {\n throw new CollabError(`Failed to persist snapshot for room \"${this.roomId}\".`, {\n code: \"COLLAB_SNAPSHOT_FAILED\",\n suggestion:\n \"Verify the configured SnapshotStore is reachable (DB/Redis up, \" +\n \"credentials valid). The in-memory default never fails; a custom \" +\n \"adapter likely threw.\",\n cause,\n });\n }\n }\n\n /** Hydrate this doc from persisted state, if any. Internal to the hub. */\n async _restore(): Promise<void> {\n try {\n const persisted = await this.store.load(this.tenantId, this.roomId);\n if (persisted) Y.applyUpdate(this.doc, persisted, REMOTE_ORIGIN);\n } catch (cause) {\n throw new CollabError(`Failed to restore room \"${this.roomId}\" from snapshot store.`, {\n code: \"COLLAB_RESTORE_FAILED\",\n suggestion:\n \"Check the SnapshotStore adapter's load() — it should resolve \" +\n \"null (not throw) when no snapshot exists for the tenant+room.\",\n cause,\n });\n }\n }\n\n destroy(): void {\n if (this.destroyed) return;\n this.destroyed = true;\n this.unsubTransport();\n this.listeners.clear();\n this.doc.destroy();\n }\n\n private assertLive(): void {\n if (this.destroyed) {\n throw new CollabError(`Room \"${this.roomId}\" was destroyed and can no longer be used.`, {\n code: \"COLLAB_DESTROYED\",\n suggestion:\n \"Acquire a fresh room via hub.room(tenantId, roomId) instead of \" +\n \"reusing a destroyed instance.\",\n });\n }\n }\n}\n","/**\n * `CollabHub` — the tenant-partition boundary.\n *\n * TENANT ISOLATION IS STRUCTURAL: rooms are stored in a `Map` keyed by a\n * composite `tenantId\u0000roomId` string with a NUL separator that cannot\n * appear in a normal id. There is no API that takes only a roomId, so a key\n * minted for tenant A is unreachable from tenant B — a room handle is only\n * ever produced by passing an explicit `tenantId`, and the snapshot store +\n * transport are likewise addressed by (tenantId, roomId). The same property\n * a Prisma adapter gets from RLS is enforced here by the composite key, with\n * no trust placed in caller-supplied payload.\n */\n\nimport { CollabError } from \"./errors\";\nimport { Room } from \"./room\";\nimport { InMemorySnapshotStore } from \"./store/memory\";\nimport { LoopbackTransport } from \"./transport/loopback\";\nimport type {\n CollabConfig,\n CollabHub,\n CollabRoom,\n CollabTransport,\n DoctorReport,\n SnapshotStore,\n} from \"./types\";\n\n// NUL separator: it cannot appear in a normal tenant/room id, so the pairs\n// (\"a\",\"bc\") and (\"ab\",\"c\") can never collide into the same room key — the\n// tenant partition is exact, not merely conventional.\nconst KEY_SEP = \"\u0000\";\n\nfunction roomKey(tenantId: string, roomId: string): string {\n return `${tenantId}${KEY_SEP}${roomId}`;\n}\n\nfunction assertId(value: string, kind: \"tenant\" | \"room\"): void {\n if (typeof value !== \"string\" || value.trim().length === 0) {\n throw new CollabError(`A non-empty ${kind}Id is required.`, {\n code: kind === \"tenant\" ? \"COLLAB_INVALID_TENANT\" : \"COLLAB_INVALID_ROOM\",\n suggestion:\n kind === \"tenant\"\n ? \"Pass the current tenant id, e.g. from getCurrentTenant().tenantId — \" +\n \"never call room() without an explicit tenant.\"\n : \"Pass a stable, non-empty roomId (document/canvas id).\",\n });\n }\n}\n\nclass Hub implements CollabHub {\n private readonly rooms = new Map<string, Room>();\n private readonly store: SnapshotStore;\n private readonly transport: CollabTransport;\n\n constructor(config: CollabConfig = {}) {\n this.store = config.store ?? new InMemorySnapshotStore();\n this.transport = config.transport ?? new LoopbackTransport();\n }\n\n room(tenantId: string, roomId: string): CollabRoom {\n assertId(tenantId, \"tenant\");\n assertId(roomId, \"room\");\n const key = roomKey(tenantId, roomId);\n let room = this.rooms.get(key);\n if (!room) {\n room = new Room(tenantId, roomId, this.store, this.transport);\n this.rooms.set(key, room);\n }\n return room;\n }\n\n async roomRestored(tenantId: string, roomId: string): Promise<CollabRoom> {\n const key = roomKey(tenantId, roomId);\n const existed = this.rooms.has(key);\n const room = this.room(tenantId, roomId) as Room;\n // Only hydrate a freshly created room; an already-live room is the\n // authoritative in-memory state.\n if (!existed) await room._restore();\n return room;\n }\n\n async doctor(): Promise<DoctorReport> {\n const start = Date.now();\n\n // 1. Yjs presence + a real round-trip (not just \"is it imported\").\n let yjsOk = false;\n let yjsDetail = \"\";\n try {\n const Y = await import(\"yjs\");\n const probe = new Y.Doc();\n probe.getMap(\"p\").set(\"k\", 1);\n const restored = new Y.Doc();\n Y.applyUpdate(restored, Y.encodeStateAsUpdate(probe));\n const ok = restored.getMap(\"p\").get(\"k\") === 1;\n probe.destroy();\n restored.destroy();\n yjsOk = ok;\n yjsDetail = ok\n ? \"Yjs encode/apply round-trip succeeded.\"\n : \"Yjs round-trip produced unexpected state.\";\n } catch (e) {\n yjsDetail = `Yjs unavailable: ${String(e)}`;\n }\n const yjs = { ok: yjsOk, detail: yjsDetail };\n\n // 2. Store health — write+read under a reserved diagnostic tenant.\n let storeOk = false;\n let storeDetail = \"\";\n try {\n const probe = new Uint8Array([7, 7, 7]);\n await this.store.save(\"__collab_doctor__\", \"__probe__\", probe);\n const back = await this.store.load(\"__collab_doctor__\", \"__probe__\");\n const ok = !!back && back.length === probe.length && back[0] === 7;\n storeOk = ok;\n storeDetail = ok\n ? \"SnapshotStore save/load round-trip succeeded.\"\n : \"SnapshotStore returned unexpected bytes.\";\n } catch (e) {\n storeDetail = `SnapshotStore error: ${String(e)}`;\n }\n const store = { ok: storeOk, detail: storeDetail };\n\n // 3. Transport health — subscribe, broadcast, observe, unsubscribe.\n let transportOk = false;\n let transportDetail = \"\";\n try {\n let received = false;\n const off = this.transport.subscribe(\"__collab_doctor__\", \"__probe__\", () => {\n received = true;\n });\n await this.transport.broadcast(\"__collab_doctor__\", \"__probe__\", new Uint8Array([1]));\n off();\n transportOk = received;\n transportDetail = received\n ? \"Transport broadcast/subscribe round-trip succeeded.\"\n : \"Transport did not deliver the probe (network adapter may be async).\";\n } catch (e) {\n transportDetail = `Transport error: ${String(e)}`;\n }\n const transport = { ok: transportOk, detail: transportDetail };\n\n return {\n ok: yjs.ok && store.ok && transport.ok,\n durationMs: Date.now() - start,\n checks: { yjs, store, transport },\n };\n }\n\n destroy(): void {\n for (const room of this.rooms.values()) room.destroy();\n this.rooms.clear();\n }\n}\n\n/** Synchronous factory. */\nexport function createCollab(config?: CollabConfig): CollabHub {\n return new Hub(config);\n}\n\n/**\n * Async factory mirroring other Sailor integration packages. There is no\n * env/credential negotiation for the zero-config defaults, so this resolves\n * immediately — the async shape is reserved for future credentialed\n * store/transport providers without a breaking signature change.\n */\nexport async function getCollab(config?: CollabConfig): Promise<CollabHub> {\n return createCollab(config);\n}\n"],"mappings":";;;;;;;;AAYA,SAAS,uBAAuB;AAkBzB,IAAM,cAAN,cAA0B,gBAAgB;AAAA,EAG/C,YAAY,SAAiB,MAAuB;AAClD,UAAM,SAAS,MAAM;AAAA,MACnB,MAAM;AAAA,MACN,yBACE;AAAA,IAEJ,CAAC;AAAA,EACH;AACF;;;AC9BA,SAAS,sBAAsB;AAC/B,YAAY,OAAO;AAKnB,IAAM,gBAAgB,uBAAO,eAAe;AAErC,IAAM,OAAN,MAAiC;AAAA,EAMtC,YACW,UACA,QACQ,OACA,WACjB;AAJS;AACA;AACQ;AACA;AAEjB,SAAK,MAAM,IAAM,MAAI;AAKrB,SAAK,IAAI,GAAG,UAAU,CAAC,QAAoB,WAAoB;AAC7D,iBAAW,MAAM,CAAC,GAAG,KAAK,SAAS,EAAG,IAAG,QAAQ,MAAM;AACvD,UAAI,WAAW,eAAe;AAC5B,aAAK,QAAQ,QAAQ,KAAK,UAAU,UAAU,KAAK,UAAU,KAAK,QAAQ,MAAM,CAAC,EAAE;AAAA,UACjF,MAAM;AAAA,UAIN;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAGD,SAAK,iBAAiB,KAAK,UAAU,UAAU,KAAK,UAAU,KAAK,QAAQ,CAAC,WAAW;AACrF,UAAI,KAAK,UAAW;AACpB,MAAE,cAAY,KAAK,KAAK,QAAQ,aAAa;AAAA,IAC/C,CAAC;AAAA,EACH;AAAA,EA5BW;AAAA,EACA;AAAA,EACQ;AAAA,EACA;AAAA,EATV;AAAA,EACQ,YAAY,oBAAI,IAAoB;AAAA,EACpC;AAAA,EACT,YAAY;AAAA,EAiCpB,YAAY,QAAoB,QAAwB;AACtD,SAAK,WAAW;AAChB,IAAE,cAAY,KAAK,KAAK,QAAQ,MAAM;AAAA,EACxC;AAAA,EAEA,cAA0B;AACxB,SAAK,WAAW;AAChB,WAAS,sBAAoB,KAAK,GAAG;AAAA,EACvC;AAAA,EAEA,SAAS,IAAgC;AACvC,SAAK,WAAW;AAChB,SAAK,UAAU,IAAI,EAAE;AACrB,WAAO,MAAM;AACX,WAAK,UAAU,OAAO,EAAE;AAAA,IAC1B;AAAA,EACF;AAAA,EAEA,MAAM,WAA0B;AAC9B,SAAK,WAAW;AAChB,UAAM,QAAQ,KAAK,YAAY;AAC/B,QAAI;AAGF,YAAM;AAAA,QAAe,KAAK;AAAA,QAAU,KAAK;AAAA,QAAQ,MAC/C,KAAK,MAAM,KAAK,KAAK,UAAU,KAAK,QAAQ,KAAK;AAAA,MACnD;AAAA,IACF,SAAS,OAAO;AACd,YAAM,IAAI,YAAY,wCAAwC,KAAK,MAAM,MAAM;AAAA,QAC7E,MAAM;AAAA,QACN,YACE;AAAA,QAGF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA,EAGA,MAAM,WAA0B;AAC9B,QAAI;AACF,YAAM,YAAY,MAAM,KAAK,MAAM,KAAK,KAAK,UAAU,KAAK,MAAM;AAClE,UAAI,UAAW,CAAE,cAAY,KAAK,KAAK,WAAW,aAAa;AAAA,IACjE,SAAS,OAAO;AACd,YAAM,IAAI,YAAY,2BAA2B,KAAK,MAAM,0BAA0B;AAAA,QACpF,MAAM;AAAA,QACN,YACE;AAAA,QAEF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAAA,EAEA,UAAgB;AACd,QAAI,KAAK,UAAW;AACpB,SAAK,YAAY;AACjB,SAAK,eAAe;AACpB,SAAK,UAAU,MAAM;AACrB,SAAK,IAAI,QAAQ;AAAA,EACnB;AAAA,EAEQ,aAAmB;AACzB,QAAI,KAAK,WAAW;AAClB,YAAM,IAAI,YAAY,SAAS,KAAK,MAAM,8CAA8C;AAAA,QACtF,MAAM;AAAA,QACN,YACE;AAAA,MAEJ,CAAC;AAAA,IACH;AAAA,EACF;AACF;;;ACpGA,IAAM,UAAU;AAEhB,SAAS,QAAQ,UAAkB,QAAwB;AACzD,SAAO,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM;AACvC;AAEA,SAAS,SAAS,OAAe,MAA+B;AAC9D,MAAI,OAAO,UAAU,YAAY,MAAM,KAAK,EAAE,WAAW,GAAG;AAC1D,UAAM,IAAI,YAAY,eAAe,IAAI,mBAAmB;AAAA,MAC1D,MAAM,SAAS,WAAW,0BAA0B;AAAA,MACpD,YACE,SAAS,WACL,2HAEA;AAAA,IACR,CAAC;AAAA,EACH;AACF;AAEA,IAAM,MAAN,MAA+B;AAAA,EACZ,QAAQ,oBAAI,IAAkB;AAAA,EAC9B;AAAA,EACA;AAAA,EAEjB,YAAY,SAAuB,CAAC,GAAG;AACrC,SAAK,QAAQ,OAAO,SAAS,IAAI,sBAAsB;AACvD,SAAK,YAAY,OAAO,aAAa,IAAI,kBAAkB;AAAA,EAC7D;AAAA,EAEA,KAAK,UAAkB,QAA4B;AACjD,aAAS,UAAU,QAAQ;AAC3B,aAAS,QAAQ,MAAM;AACvB,UAAM,MAAM,QAAQ,UAAU,MAAM;AACpC,QAAI,OAAO,KAAK,MAAM,IAAI,GAAG;AAC7B,QAAI,CAAC,MAAM;AACT,aAAO,IAAI,KAAK,UAAU,QAAQ,KAAK,OAAO,KAAK,SAAS;AAC5D,WAAK,MAAM,IAAI,KAAK,IAAI;AAAA,IAC1B;AACA,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,aAAa,UAAkB,QAAqC;AACxE,UAAM,MAAM,QAAQ,UAAU,MAAM;AACpC,UAAM,UAAU,KAAK,MAAM,IAAI,GAAG;AAClC,UAAM,OAAO,KAAK,KAAK,UAAU,MAAM;AAGvC,QAAI,CAAC,QAAS,OAAM,KAAK,SAAS;AAClC,WAAO;AAAA,EACT;AAAA,EAEA,MAAM,SAAgC;AACpC,UAAM,QAAQ,KAAK,IAAI;AAGvB,QAAI,QAAQ;AACZ,QAAI,YAAY;AAChB,QAAI;AACF,YAAMA,KAAI,MAAM,OAAO,KAAK;AAC5B,YAAM,QAAQ,IAAIA,GAAE,IAAI;AACxB,YAAM,OAAO,GAAG,EAAE,IAAI,KAAK,CAAC;AAC5B,YAAM,WAAW,IAAIA,GAAE,IAAI;AAC3B,MAAAA,GAAE,YAAY,UAAUA,GAAE,oBAAoB,KAAK,CAAC;AACpD,YAAM,KAAK,SAAS,OAAO,GAAG,EAAE,IAAI,GAAG,MAAM;AAC7C,YAAM,QAAQ;AACd,eAAS,QAAQ;AACjB,cAAQ;AACR,kBAAY,KACR,2CACA;AAAA,IACN,SAAS,GAAG;AACV,kBAAY,oBAAoB,OAAO,CAAC,CAAC;AAAA,IAC3C;AACA,UAAM,MAAM,EAAE,IAAI,OAAO,QAAQ,UAAU;AAG3C,QAAI,UAAU;AACd,QAAI,cAAc;AAClB,QAAI;AACF,YAAM,QAAQ,IAAI,WAAW,CAAC,GAAG,GAAG,CAAC,CAAC;AACtC,YAAM,KAAK,MAAM,KAAK,qBAAqB,aAAa,KAAK;AAC7D,YAAM,OAAO,MAAM,KAAK,MAAM,KAAK,qBAAqB,WAAW;AACnE,YAAM,KAAK,CAAC,CAAC,QAAQ,KAAK,WAAW,MAAM,UAAU,KAAK,CAAC,MAAM;AACjE,gBAAU;AACV,oBAAc,KACV,kDACA;AAAA,IACN,SAAS,GAAG;AACV,oBAAc,wBAAwB,OAAO,CAAC,CAAC;AAAA,IACjD;AACA,UAAM,QAAQ,EAAE,IAAI,SAAS,QAAQ,YAAY;AAGjD,QAAI,cAAc;AAClB,QAAI,kBAAkB;AACtB,QAAI;AACF,UAAI,WAAW;AACf,YAAM,MAAM,KAAK,UAAU,UAAU,qBAAqB,aAAa,MAAM;AAC3E,mBAAW;AAAA,MACb,CAAC;AACD,YAAM,KAAK,UAAU,UAAU,qBAAqB,aAAa,IAAI,WAAW,CAAC,CAAC,CAAC,CAAC;AACpF,UAAI;AACJ,oBAAc;AACd,wBAAkB,WACd,wDACA;AAAA,IACN,SAAS,GAAG;AACV,wBAAkB,oBAAoB,OAAO,CAAC,CAAC;AAAA,IACjD;AACA,UAAM,YAAY,EAAE,IAAI,aAAa,QAAQ,gBAAgB;AAE7D,WAAO;AAAA,MACL,IAAI,IAAI,MAAM,MAAM,MAAM,UAAU;AAAA,MACpC,YAAY,KAAK,IAAI,IAAI;AAAA,MACzB,QAAQ,EAAE,KAAK,OAAO,UAAU;AAAA,IAClC;AAAA,EACF;AAAA,EAEA,UAAgB;AACd,eAAW,QAAQ,KAAK,MAAM,OAAO,EAAG,MAAK,QAAQ;AACrD,SAAK,MAAM,MAAM;AAAA,EACnB;AACF;AAGO,SAAS,aAAa,QAAkC;AAC7D,SAAO,IAAI,IAAI,MAAM;AACvB;AAQA,eAAsB,UAAU,QAA2C;AACzE,SAAO,aAAa,MAAM;AAC5B;","names":["Y"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nebutra/collab",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"description": "Multi-tenant, transport-agnostic real-time collaborative sync layer: tenant-partitioned CRDT rooms (Yjs) with pluggable snapshot store + transport seams",
|
|
5
5
|
"private": false,
|
|
6
6
|
"license": "MIT",
|
|
@@ -19,11 +19,11 @@
|
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"yjs": "^13.6.30",
|
|
22
|
-
"@nebutra/capability-kit": "0.2.
|
|
23
|
-
"@nebutra/tenant-store": "0.2.
|
|
22
|
+
"@nebutra/capability-kit": "0.2.2",
|
|
23
|
+
"@nebutra/tenant-store": "0.2.2"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@types/node": "^
|
|
26
|
+
"@types/node": "^25.9.1",
|
|
27
27
|
"@vitest/coverage-v8": "^4.0.18",
|
|
28
28
|
"tsup": "^8.5.1",
|
|
29
29
|
"typescript": "^5.9.3",
|
package/src/hub.ts
CHANGED
|
Binary file
|