zero-query 1.0.9 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +2 -0
  3. package/cli/args.js +33 -33
  4. package/cli/commands/build-api.js +443 -0
  5. package/cli/commands/build.js +254 -216
  6. package/cli/commands/bundle.js +1228 -1183
  7. package/cli/commands/create.js +137 -121
  8. package/cli/commands/dev/devtools/index.js +56 -56
  9. package/cli/commands/dev/devtools/js/components.js +49 -49
  10. package/cli/commands/dev/devtools/js/core.js +423 -423
  11. package/cli/commands/dev/devtools/js/elements.js +421 -421
  12. package/cli/commands/dev/devtools/js/network.js +166 -166
  13. package/cli/commands/dev/devtools/js/performance.js +73 -73
  14. package/cli/commands/dev/devtools/js/router.js +105 -105
  15. package/cli/commands/dev/devtools/js/source.js +132 -132
  16. package/cli/commands/dev/devtools/js/stats.js +35 -35
  17. package/cli/commands/dev/devtools/js/tabs.js +79 -79
  18. package/cli/commands/dev/devtools/panel.html +95 -95
  19. package/cli/commands/dev/devtools/styles.css +244 -244
  20. package/cli/commands/dev/index.js +107 -107
  21. package/cli/commands/dev/logger.js +75 -75
  22. package/cli/commands/dev/overlay.js +858 -858
  23. package/cli/commands/dev/server.js +220 -167
  24. package/cli/commands/dev/validator.js +94 -94
  25. package/cli/commands/dev/watcher.js +172 -172
  26. package/cli/help.js +114 -112
  27. package/cli/index.js +52 -52
  28. package/cli/scaffold/default/LICENSE +21 -21
  29. package/cli/scaffold/default/app/app.js +207 -207
  30. package/cli/scaffold/default/app/components/about.js +201 -201
  31. package/cli/scaffold/default/app/components/api-demo.js +143 -143
  32. package/cli/scaffold/default/app/components/contact-card.js +231 -231
  33. package/cli/scaffold/default/app/components/contacts/contacts.css +706 -706
  34. package/cli/scaffold/default/app/components/contacts/contacts.html +200 -200
  35. package/cli/scaffold/default/app/components/contacts/contacts.js +196 -196
  36. package/cli/scaffold/default/app/components/counter.js +127 -127
  37. package/cli/scaffold/default/app/components/home.js +249 -249
  38. package/cli/scaffold/default/app/components/not-found.js +16 -16
  39. package/cli/scaffold/default/app/components/playground/playground.css +115 -115
  40. package/cli/scaffold/default/app/components/playground/playground.html +161 -161
  41. package/cli/scaffold/default/app/components/playground/playground.js +116 -116
  42. package/cli/scaffold/default/app/components/todos.js +225 -225
  43. package/cli/scaffold/default/app/components/toolkit/toolkit.css +97 -97
  44. package/cli/scaffold/default/app/components/toolkit/toolkit.html +146 -146
  45. package/cli/scaffold/default/app/components/toolkit/toolkit.js +280 -280
  46. package/cli/scaffold/default/app/routes.js +15 -15
  47. package/cli/scaffold/default/app/store.js +101 -101
  48. package/cli/scaffold/default/global.css +552 -552
  49. package/cli/scaffold/default/index.html +99 -99
  50. package/cli/scaffold/minimal/app/app.js +85 -85
  51. package/cli/scaffold/minimal/app/components/about.js +68 -68
  52. package/cli/scaffold/minimal/app/components/counter.js +122 -122
  53. package/cli/scaffold/minimal/app/components/home.js +68 -68
  54. package/cli/scaffold/minimal/app/components/not-found.js +16 -16
  55. package/cli/scaffold/minimal/app/routes.js +9 -9
  56. package/cli/scaffold/minimal/app/store.js +36 -36
  57. package/cli/scaffold/minimal/global.css +300 -300
  58. package/cli/scaffold/minimal/index.html +44 -44
  59. package/cli/scaffold/ssr/app/app.js +41 -41
  60. package/cli/scaffold/ssr/app/components/about.js +55 -55
  61. package/cli/scaffold/ssr/app/components/blog/index.js +65 -65
  62. package/cli/scaffold/ssr/app/components/blog/post.js +86 -86
  63. package/cli/scaffold/ssr/app/components/home.js +37 -37
  64. package/cli/scaffold/ssr/app/components/not-found.js +15 -15
  65. package/cli/scaffold/ssr/app/routes.js +8 -8
  66. package/cli/scaffold/ssr/global.css +228 -228
  67. package/cli/scaffold/ssr/index.html +37 -37
  68. package/cli/scaffold/ssr/package.json +8 -8
  69. package/cli/scaffold/ssr/server/data/posts.js +144 -144
  70. package/cli/scaffold/ssr/server/index.js +213 -213
  71. package/cli/scaffold/webrtc/app/app.js +11 -0
  72. package/cli/scaffold/webrtc/app/components/video-room.js +295 -0
  73. package/cli/scaffold/webrtc/app/lib/room.js +252 -0
  74. package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
  75. package/cli/scaffold/webrtc/global.css +250 -0
  76. package/cli/scaffold/webrtc/index.html +21 -0
  77. package/cli/utils.js +305 -287
  78. package/dist/API.md +7264 -0
  79. package/dist/zquery.dist.zip +0 -0
  80. package/dist/zquery.js +10313 -6252
  81. package/dist/zquery.min.js +8 -601
  82. package/index.d.ts +570 -365
  83. package/index.js +311 -232
  84. package/package.json +76 -69
  85. package/src/component.js +1709 -1454
  86. package/src/core.js +921 -921
  87. package/src/diff.js +497 -497
  88. package/src/errors.js +209 -209
  89. package/src/expression.js +922 -922
  90. package/src/http.js +242 -242
  91. package/src/package.json +1 -1
  92. package/src/reactive.js +255 -254
  93. package/src/router.js +843 -773
  94. package/src/ssr.js +418 -418
  95. package/src/store.js +318 -272
  96. package/src/utils.js +515 -515
  97. package/src/webrtc/e2ee.js +351 -0
  98. package/src/webrtc/errors.js +116 -0
  99. package/src/webrtc/ice.js +301 -0
  100. package/src/webrtc/index.js +131 -0
  101. package/src/webrtc/joinToken.js +119 -0
  102. package/src/webrtc/observe.js +172 -0
  103. package/src/webrtc/peer.js +351 -0
  104. package/src/webrtc/reactive.js +268 -0
  105. package/src/webrtc/room.js +625 -0
  106. package/src/webrtc/sdp.js +302 -0
  107. package/src/webrtc/sfu/index.js +43 -0
  108. package/src/webrtc/sfu/livekit.js +131 -0
  109. package/src/webrtc/sfu/mediasoup.js +150 -0
  110. package/src/webrtc/signaling.js +373 -0
  111. package/src/webrtc/turn.js +237 -0
  112. package/tests/_helpers/webrtcFakes.js +289 -0
  113. package/tests/audit.test.js +4158 -4158
  114. package/tests/cli.test.js +1136 -1023
  115. package/tests/compare.test.js +497 -0
  116. package/tests/component.test.js +3969 -3938
  117. package/tests/core.test.js +1910 -1910
  118. package/tests/dev-server.test.js +489 -0
  119. package/tests/diff.test.js +1416 -1416
  120. package/tests/docs.test.js +1664 -0
  121. package/tests/electron-features.test.js +864 -0
  122. package/tests/errors.test.js +619 -619
  123. package/tests/expression.test.js +1056 -1056
  124. package/tests/http.test.js +648 -648
  125. package/tests/reactive.test.js +819 -819
  126. package/tests/router.test.js +2327 -2327
  127. package/tests/ssr.test.js +870 -870
  128. package/tests/store.test.js +830 -830
  129. package/tests/test-minifier.js +153 -153
  130. package/tests/test-ssr.js +27 -27
  131. package/tests/utils.test.js +1377 -1377
  132. package/tests/webrtc/e2ee.test.js +283 -0
  133. package/tests/webrtc/ice.test.js +202 -0
  134. package/tests/webrtc/joinToken.test.js +89 -0
  135. package/tests/webrtc/observe.test.js +111 -0
  136. package/tests/webrtc/peer.test.js +373 -0
  137. package/tests/webrtc/reactive.test.js +235 -0
  138. package/tests/webrtc/room.test.js +406 -0
  139. package/tests/webrtc/sdp.test.js +151 -0
  140. package/tests/webrtc/sfu-livekit.test.js +119 -0
  141. package/tests/webrtc/sfu.test.js +160 -0
  142. package/tests/webrtc/signaling.test.js +251 -0
  143. package/tests/webrtc/turn.test.js +256 -0
  144. package/types/collection.d.ts +383 -383
  145. package/types/component.d.ts +186 -186
  146. package/types/errors.d.ts +135 -135
  147. package/types/http.d.ts +92 -92
  148. package/types/misc.d.ts +201 -201
  149. package/types/reactive.d.ts +98 -98
  150. package/types/router.d.ts +190 -190
  151. package/types/ssr.d.ts +102 -102
  152. package/types/store.d.ts +146 -145
  153. package/types/utils.d.ts +245 -245
  154. package/types/webrtc.d.ts +653 -0
@@ -0,0 +1,302 @@
1
+ /**
2
+ * src/webrtc/sdp.js - minimal read-only SDP helpers
3
+ *
4
+ * A trimmed port of the server-side `@zero-server/webrtc` SDP module.
5
+ * The client only needs to read a few keys out of an SDP (for stats,
6
+ * ICE bookkeeping, and a sanity check before sending it through
7
+ * signaling); full RFC 8866 conformance lives on the server.
8
+ *
9
+ * Surface:
10
+ * - `parseSdp(text)` -> structured `{ version, origin, sessionName, media: [...] }`
11
+ * - `validateSdp(text)` -> throws `SdpError` if the SDP would be rejected by the hub
12
+ * (missing `UDP/TLS/RTP/SAVPF` proto, `ice-ufrag`, `ice-pwd`,
13
+ * or `fingerprint` on any non-rejected m-section).
14
+ *
15
+ * Pure functions, no globals, SSR-safe.
16
+ */
17
+
18
+ import { SdpError } from './errors.js';
19
+
20
+
21
+ /** Max SDP size accepted by the server. */
22
+ const DEFAULT_MAX_BYTES = 65_536;
23
+
24
+ /** Required transport protocol per server validator. */
25
+ const REQUIRED_PROTO = 'UDP/TLS/RTP/SAVPF';
26
+
27
+ /** Valid SDP direction attributes (RFC 8866 §6.7). */
28
+ export const SDP_DIRECTIONS = Object.freeze(['sendrecv', 'sendonly', 'recvonly', 'inactive']);
29
+
30
+
31
+ /**
32
+ * Parse an SDP document into a minimal, structured form.
33
+ *
34
+ * Only the fields the client needs are lifted onto named keys; everything
35
+ * else is preserved verbatim on `attributes`/`media[i].attributes` so the
36
+ * caller can still inspect unusual lines without losing information.
37
+ *
38
+ * @param {string} text
39
+ * @param {object} [opts]
40
+ * @param {number} [opts.maxBytes=65536]
41
+ * @returns {{
42
+ * version: number,
43
+ * origin: ?object,
44
+ * sessionName: string,
45
+ * attributes: Array<{key:string,value:string}>,
46
+ * media: Array<{
47
+ * kind: string,
48
+ * port: number,
49
+ * proto: string,
50
+ * fmts: string[],
51
+ * mid?: string,
52
+ * iceUfrag?: string,
53
+ * icePwd?: string,
54
+ * fingerprint?: { algorithm: string, value: string },
55
+ * setup?: string,
56
+ * direction?: string,
57
+ * rtcpMux: boolean,
58
+ * candidates: string[],
59
+ * rtpmaps: Array<{ payload: number, codec: string, clockRate: number, channels?: number }>,
60
+ * attributes: Array<{key:string,value:string}>,
61
+ * }>,
62
+ * }}
63
+ */
64
+ export function parseSdp(text, opts = {}) {
65
+ if (typeof text !== 'string') {
66
+ throw new SdpError('parseSdp: input must be a string', { code: 'ZQ_WEBRTC_SDP_PARSE' });
67
+ }
68
+ const maxBytes = typeof opts.maxBytes === 'number' ? opts.maxBytes : DEFAULT_MAX_BYTES;
69
+ if (text.length > maxBytes) {
70
+ throw new SdpError(`parseSdp: payload exceeds ${maxBytes} bytes`, { code: 'ZQ_WEBRTC_SDP_TOO_LARGE' });
71
+ }
72
+ if (text.length === 0) {
73
+ throw new SdpError('parseSdp: empty input', { code: 'ZQ_WEBRTC_SDP_PARSE' });
74
+ }
75
+
76
+ const lines = text.replace(/\r\n/g, '\n').split('\n').filter((l) => l.length > 0);
77
+ if (lines.length === 0) {
78
+ throw new SdpError('parseSdp: no non-empty lines', { code: 'ZQ_WEBRTC_SDP_PARSE' });
79
+ }
80
+
81
+ const session = {
82
+ version: 0,
83
+ origin: null,
84
+ sessionName: '',
85
+ attributes: [],
86
+ media: [],
87
+ };
88
+
89
+ let current = session;
90
+ let currentMedia = null;
91
+
92
+ for (let i = 0; i < lines.length; i++) {
93
+ const raw = lines[i];
94
+ const eq = raw.indexOf('=');
95
+ if (eq < 1) {
96
+ throw new SdpError(`parseSdp: malformed line ${i + 1}`, {
97
+ code: 'ZQ_WEBRTC_SDP_PARSE',
98
+ context: { line: i + 1 },
99
+ });
100
+ }
101
+ const type = raw.slice(0, eq);
102
+ const val = raw.slice(eq + 1);
103
+
104
+ if (i === 0 && type !== 'v') {
105
+ throw new SdpError('parseSdp: SDP must start with v=', {
106
+ code: 'ZQ_WEBRTC_SDP_PARSE',
107
+ context: { line: 1 },
108
+ });
109
+ }
110
+
111
+ switch (type) {
112
+ case 'v':
113
+ session.version = Number(val);
114
+ break;
115
+ case 'o':
116
+ session.origin = _parseOrigin(val);
117
+ break;
118
+ case 's':
119
+ session.sessionName = val;
120
+ break;
121
+ case 'm': {
122
+ currentMedia = _newMedia(val);
123
+ session.media.push(currentMedia);
124
+ current = currentMedia;
125
+ break;
126
+ }
127
+ case 'a':
128
+ _applyAttribute(current, val);
129
+ break;
130
+ default:
131
+ // ignore the lines we don't need for the client subset
132
+ break;
133
+ }
134
+ }
135
+
136
+ return session;
137
+ }
138
+
139
+
140
+ /**
141
+ * Validate that the SDP would survive the server-side hub's strict checks.
142
+ * Throws `SdpError` on missing required attributes; returns the parsed
143
+ * structure on success so callers can chain.
144
+ *
145
+ * Required, per `@zero-server/webrtc` signaling validator:
146
+ * - at least one m-line
147
+ * - non-rejected m-lines (port != 0) must use `UDP/TLS/RTP/SAVPF`
148
+ * - each non-rejected m-line must carry `ice-ufrag`, `ice-pwd`, and
149
+ * a `fingerprint` attribute (session-level fallback is honored).
150
+ *
151
+ * @param {string} text
152
+ * @returns {ReturnType<typeof parseSdp>}
153
+ */
154
+ export function validateSdp(text) {
155
+ const parsed = parseSdp(text);
156
+ if (parsed.media.length === 0) {
157
+ throw new SdpError('validateSdp: SDP has no m-lines', { code: 'ZQ_WEBRTC_SDP_NO_MEDIA' });
158
+ }
159
+
160
+ // Session-level fallbacks: ice-ufrag / ice-pwd / fingerprint can appear
161
+ // once at the session level and apply to every m-section.
162
+ const sessIceUfrag = _findAttr(parsed.attributes, 'ice-ufrag');
163
+ const sessIcePwd = _findAttr(parsed.attributes, 'ice-pwd');
164
+ const sessFingerprint = _findAttr(parsed.attributes, 'fingerprint');
165
+
166
+ for (let i = 0; i < parsed.media.length; i++) {
167
+ const m = parsed.media[i];
168
+ if (m.port === 0) continue;
169
+ if (m.proto !== REQUIRED_PROTO) {
170
+ throw new SdpError(
171
+ `validateSdp: m-line ${i} proto "${m.proto}" must be "${REQUIRED_PROTO}"`,
172
+ { code: 'ZQ_WEBRTC_SDP_BAD_PROTO', context: { index: i, proto: m.proto } }
173
+ );
174
+ }
175
+ const ufrag = m.iceUfrag || sessIceUfrag;
176
+ const pwd = m.icePwd || sessIcePwd;
177
+ const fp = m.fingerprint || sessFingerprint;
178
+ if (!ufrag) {
179
+ throw new SdpError(`validateSdp: m-line ${i} missing ice-ufrag`, {
180
+ code: 'ZQ_WEBRTC_SDP_NO_ICE_UFRAG', context: { index: i },
181
+ });
182
+ }
183
+ if (!pwd) {
184
+ throw new SdpError(`validateSdp: m-line ${i} missing ice-pwd`, {
185
+ code: 'ZQ_WEBRTC_SDP_NO_ICE_PWD', context: { index: i },
186
+ });
187
+ }
188
+ if (!fp) {
189
+ throw new SdpError(`validateSdp: m-line ${i} missing fingerprint`, {
190
+ code: 'ZQ_WEBRTC_SDP_NO_FINGERPRINT', context: { index: i },
191
+ });
192
+ }
193
+ }
194
+
195
+ return parsed;
196
+ }
197
+
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Internals
201
+ // ---------------------------------------------------------------------------
202
+
203
+ /** @param {string} val */
204
+ function _parseOrigin(val) {
205
+ const t = val.split(/\s+/);
206
+ if (t.length < 6) return null;
207
+ return {
208
+ username: t[0],
209
+ sessionId: t[1],
210
+ sessionVersion: Number(t[2]),
211
+ netType: t[3],
212
+ addrType: t[4],
213
+ address: t[5],
214
+ };
215
+ }
216
+
217
+ /** @param {string} val - the part after `m=` */
218
+ function _newMedia(val) {
219
+ const t = val.split(/\s+/);
220
+ return {
221
+ kind: t[0] || '',
222
+ port: Number(t[1]) || 0,
223
+ proto: t[2] || '',
224
+ fmts: t.slice(3),
225
+ mid: undefined,
226
+ iceUfrag: undefined,
227
+ icePwd: undefined,
228
+ fingerprint: undefined,
229
+ setup: undefined,
230
+ direction: undefined,
231
+ rtcpMux: false,
232
+ candidates: [],
233
+ rtpmaps: [],
234
+ attributes: [],
235
+ };
236
+ }
237
+
238
+ /**
239
+ * Apply a single `a=...` line to the current section (session or media).
240
+ * @param {object} section
241
+ * @param {string} val
242
+ */
243
+ function _applyAttribute(section, val) {
244
+ const colon = val.indexOf(':');
245
+ const key = colon === -1 ? val : val.slice(0, colon);
246
+ const value = colon === -1 ? '' : val.slice(colon + 1);
247
+ section.attributes.push({ key, value });
248
+
249
+ switch (key) {
250
+ case 'mid': if ('mid' in section) section.mid = value; break;
251
+ case 'ice-ufrag': if ('iceUfrag' in section) section.iceUfrag = value; break;
252
+ case 'ice-pwd': if ('icePwd' in section) section.icePwd = value; break;
253
+ case 'setup': if ('setup' in section) section.setup = value; break;
254
+ case 'rtcp-mux': if ('rtcpMux' in section) section.rtcpMux = true; break;
255
+ case 'fingerprint': {
256
+ const sp = value.indexOf(' ');
257
+ const fp = sp === -1
258
+ ? { algorithm: value, value: '' }
259
+ : { algorithm: value.slice(0, sp), value: value.slice(sp + 1) };
260
+ if ('fingerprint' in section) section.fingerprint = fp;
261
+ break;
262
+ }
263
+ case 'candidate':
264
+ if ('candidates' in section) section.candidates.push(`candidate:${value}`);
265
+ break;
266
+ case 'rtpmap': {
267
+ if (!('rtpmaps' in section)) break;
268
+ const sp = value.indexOf(' ');
269
+ if (sp === -1) break;
270
+ const payload = Number(value.slice(0, sp));
271
+ const desc = value.slice(sp + 1).split('/');
272
+ section.rtpmaps.push({
273
+ payload,
274
+ codec: desc[0] || '',
275
+ clockRate: Number(desc[1]) || 0,
276
+ channels: desc[2] ? Number(desc[2]) : undefined,
277
+ });
278
+ break;
279
+ }
280
+ case 'sendrecv':
281
+ case 'sendonly':
282
+ case 'recvonly':
283
+ case 'inactive':
284
+ if ('direction' in section) section.direction = key;
285
+ break;
286
+ default:
287
+ // unknown attribute - already preserved on attributes[]
288
+ break;
289
+ }
290
+ }
291
+
292
+ /**
293
+ * Find the first attribute value for `key` in an attribute list, or `undefined`.
294
+ * @param {Array<{key:string,value:string}>} list
295
+ * @param {string} key
296
+ */
297
+ function _findAttr(list, key) {
298
+ for (let i = 0; i < list.length; i++) {
299
+ if (list[i].key === key) return list[i].value || true;
300
+ }
301
+ return undefined;
302
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * src/webrtc/sfu/index.js
3
+ *
4
+ * SFU adapter registry. Adapters are dynamic-imported so the (optional)
5
+ * peer dependencies (`mediasoup-client`, `livekit-client`) only load
6
+ * when actually used.
7
+ *
8
+ * Public surface:
9
+ * - `loadSfuAdapter(name, opts?)` → `Promise<SfuAdapter>`
10
+ */
11
+
12
+ import { SfuError } from '../errors.js';
13
+ import { createMediasoupAdapter } from './mediasoup.js';
14
+ import { createLivekitAdapter } from './livekit.js';
15
+
16
+
17
+ /**
18
+ * @typedef {object} SfuAdapter
19
+ * @property {'mediasoup'|'livekit'} name
20
+ * @property {(joinOpts: any) => Promise<any>} join
21
+ */
22
+
23
+
24
+ /**
25
+ * Load an SFU adapter by name. The adapter's peer dependency must be
26
+ * installed by the consuming app.
27
+ *
28
+ * @param {'mediasoup'|'livekit'} name
29
+ * @param {object} [opts]
30
+ * @returns {Promise<SfuAdapter>}
31
+ */
32
+ export async function loadSfuAdapter(name, opts = {}) {
33
+ if (name === 'mediasoup') {
34
+ return createMediasoupAdapter(opts);
35
+ }
36
+ if (name === 'livekit') {
37
+ return createLivekitAdapter(opts);
38
+ }
39
+ throw new SfuError(`unknown SFU adapter: ${name}`, {
40
+ code: 'ZQ_WEBRTC_SFU_UNKNOWN',
41
+ context: { name },
42
+ });
43
+ }
@@ -0,0 +1,131 @@
1
+ /**
2
+ * src/webrtc/sfu/livekit.js
3
+ *
4
+ * Thin wrapper around the optional `livekit-client` peer dependency.
5
+ *
6
+ * - `livekit-client` is intentionally NOT bundled. Apps that want it
7
+ * must `npm install livekit-client` themselves.
8
+ * - If it isn't installed, `createLivekitAdapter()` throws
9
+ * `ZQ_WEBRTC_SFU_PEER_MISSING` with an actionable message.
10
+ * - The adapter exposes a LiveKit `Room` instance plus `connect()` /
11
+ * `disconnect()` helpers. The higher-level zQuery `Room` mapping
12
+ * lives in the consuming app for now; calling `.join()` throws
13
+ * `ZQ_WEBRTC_SFU_JOIN_UNAVAILABLE`.
14
+ */
15
+
16
+ import { SfuError } from '../errors.js';
17
+
18
+
19
+ /**
20
+ * Dynamically import `livekit-client`. Returns the module's exports.
21
+ * Throws `SfuError(ZQ_WEBRTC_SFU_PEER_MISSING)` if the package is absent.
22
+ *
23
+ * @returns {Promise<any>}
24
+ */
25
+ async function _importLivekitClient() {
26
+ try {
27
+ // Compose the package name at runtime so static bundlers (Vite,
28
+ // Rollup, esbuild) don't try to resolve the optional peer dep.
29
+ const pkg = ['livekit', 'client'].join('-');
30
+ return await import(/* @vite-ignore */ pkg);
31
+ } catch (cause) {
32
+ throw new SfuError(
33
+ 'livekit-client peer dependency is not installed; run `npm install livekit-client`',
34
+ { code: 'ZQ_WEBRTC_SFU_PEER_MISSING', cause },
35
+ );
36
+ }
37
+ }
38
+
39
+
40
+ /**
41
+ * Build a LiveKit-client adapter.
42
+ *
43
+ * @param {object} [opts]
44
+ * @param {any} [opts.client] Pre-imported livekit-client module (test hook).
45
+ * @param {object} [opts.roomOptions] Forwarded to `new Room(...)`.
46
+ * @returns {Promise<import('./index.js').SfuAdapter>}
47
+ */
48
+ export async function createLivekitAdapter(opts = {}) {
49
+ const mod = opts.client || await _importLivekitClient();
50
+ const RoomCtor = mod.Room || (mod.default && mod.default.Room);
51
+ if (typeof RoomCtor !== 'function') {
52
+ throw new SfuError('livekit-client module did not expose a Room constructor', {
53
+ code: 'ZQ_WEBRTC_SFU_BAD_MODULE',
54
+ });
55
+ }
56
+
57
+ let room;
58
+ try {
59
+ room = new RoomCtor(opts.roomOptions || {});
60
+ } catch (cause) {
61
+ throw new SfuError('failed to construct livekit-client Room', {
62
+ code: 'ZQ_WEBRTC_SFU_ROOM_FAILED',
63
+ cause,
64
+ });
65
+ }
66
+
67
+ let connected = false;
68
+
69
+ return {
70
+ name: 'livekit',
71
+ room,
72
+
73
+ /** Has `connect()` resolved at least once and not been undone by disconnect? */
74
+ get connected() {
75
+ return connected;
76
+ },
77
+
78
+ /**
79
+ * Connect to a LiveKit server.
80
+ * @param {string} url LiveKit signaling URL (`wss://...`).
81
+ * @param {string} token Room access token (JWT) minted server-side.
82
+ * @param {object} [connectOpts] Forwarded to `room.connect(...)`.
83
+ * @returns {Promise<void>}
84
+ */
85
+ async connect(url, token, connectOpts) {
86
+ if (typeof url !== 'string' || !url) {
87
+ throw new SfuError('connect(url, token): url required', {
88
+ code: 'ZQ_WEBRTC_SFU_BAD_URL',
89
+ });
90
+ }
91
+ if (typeof token !== 'string' || !token) {
92
+ throw new SfuError('connect(url, token): token required', {
93
+ code: 'ZQ_WEBRTC_SFU_BAD_TOKEN',
94
+ });
95
+ }
96
+ try {
97
+ await room.connect(url, token, connectOpts);
98
+ connected = true;
99
+ } catch (cause) {
100
+ throw new SfuError('livekit-client Room.connect() failed', {
101
+ code: 'ZQ_WEBRTC_SFU_CONNECT_FAILED',
102
+ cause,
103
+ });
104
+ }
105
+ },
106
+
107
+ /** Disconnect from the LiveKit server (best effort). */
108
+ async disconnect() {
109
+ if (!connected) return;
110
+ try {
111
+ await room.disconnect();
112
+ } finally {
113
+ connected = false;
114
+ }
115
+ },
116
+
117
+ /**
118
+ * Reserved for the higher-level join flow that maps a LiveKit `Room`
119
+ * to a zQuery `Room`. Not implemented yet.
120
+ *
121
+ * @param {any} _joinOpts
122
+ * @returns {Promise<never>}
123
+ */
124
+ async join(_joinOpts) {
125
+ throw new SfuError(
126
+ 'livekit adapter.join() not implemented; use connect(url, token) and the underlying livekit-client Room directly',
127
+ { code: 'ZQ_WEBRTC_SFU_JOIN_UNAVAILABLE' },
128
+ );
129
+ },
130
+ };
131
+ }
@@ -0,0 +1,150 @@
1
+ /**
2
+ * src/webrtc/sfu/mediasoup.js
3
+ *
4
+ * Thin wrapper around the optional `mediasoup-client` peer dependency.
5
+ *
6
+ * - `mediasoup-client` is intentionally NOT bundled. Apps that want it
7
+ * must `npm install mediasoup-client` themselves.
8
+ * - If it isn't installed, `createMediasoupAdapter()` throws
9
+ * `ZQ_WEBRTC_SFU_PEER_MISSING` with an actionable message.
10
+ * - The adapter exposes the lower-level mediasoup `Device` plus
11
+ * helpers for `load(routerRtpCapabilities)`, `canProduce(kind)`,
12
+ * `createSendTransport(params)`, `createRecvTransport(params)`.
13
+ * - The higher-level `adapter.join(joinOpts)` requires SFU-specific
14
+ * signaling (request → response over a data channel or HTTP). That
15
+ * glue lives in the consuming app for now; calling `.join()` throws
16
+ * `ZQ_WEBRTC_SFU_JOIN_UNAVAILABLE` so the limitation is explicit.
17
+ */
18
+
19
+ import { SfuError } from '../errors.js';
20
+
21
+
22
+ /**
23
+ * Dynamically import `mediasoup-client`. Returns the module's exports.
24
+ * Throws `SfuError(ZQ_WEBRTC_SFU_PEER_MISSING)` if the package is absent.
25
+ *
26
+ * @returns {Promise<any>}
27
+ */
28
+ async function _importMediasoupClient() {
29
+ try {
30
+ // The package name is computed at runtime so static bundlers (Vite,
31
+ // Rollup, esbuild) don't try to resolve the optional peer dep at
32
+ // build time and fail the build when it isn't installed.
33
+ const pkg = ['mediasoup', 'client'].join('-');
34
+ return await import(/* @vite-ignore */ pkg);
35
+ } catch (cause) {
36
+ throw new SfuError(
37
+ 'mediasoup-client peer dependency is not installed; run `npm install mediasoup-client`',
38
+ { code: 'ZQ_WEBRTC_SFU_PEER_MISSING', cause },
39
+ );
40
+ }
41
+ }
42
+
43
+
44
+ /**
45
+ * Build a mediasoup-client adapter.
46
+ *
47
+ * @param {object} [opts]
48
+ * @param {any} [opts.client] Pre-imported mediasoup-client module (test hook).
49
+ * @param {object} [opts.deviceOptions] Forwarded to `new Device(...)`.
50
+ * @returns {Promise<import('./index.js').SfuAdapter>}
51
+ */
52
+ export async function createMediasoupAdapter(opts = {}) {
53
+ const mod = opts.client || await _importMediasoupClient();
54
+ const Device = mod.Device || (mod.default && mod.default.Device);
55
+ if (typeof Device !== 'function') {
56
+ throw new SfuError('mediasoup-client module did not expose a Device constructor', {
57
+ code: 'ZQ_WEBRTC_SFU_BAD_MODULE',
58
+ });
59
+ }
60
+
61
+ let device;
62
+ try {
63
+ device = new Device(opts.deviceOptions || {});
64
+ } catch (cause) {
65
+ throw new SfuError('failed to construct mediasoup-client Device', {
66
+ code: 'ZQ_WEBRTC_SFU_DEVICE_FAILED',
67
+ cause,
68
+ });
69
+ }
70
+
71
+ return {
72
+ name: 'mediasoup',
73
+ device,
74
+
75
+ /** Has `device.load({ routerRtpCapabilities })` been called yet? */
76
+ get loaded() {
77
+ return !!device.loaded;
78
+ },
79
+
80
+ /**
81
+ * Load the device with the SFU router's RTP capabilities.
82
+ * @param {any} routerRtpCapabilities
83
+ * @returns {Promise<void>}
84
+ */
85
+ async load(routerRtpCapabilities) {
86
+ if (!routerRtpCapabilities || typeof routerRtpCapabilities !== 'object') {
87
+ throw new SfuError('load(routerRtpCapabilities): routerRtpCapabilities required', {
88
+ code: 'ZQ_WEBRTC_SFU_BAD_RTP_CAPS',
89
+ });
90
+ }
91
+ if (device.loaded) return;
92
+ try {
93
+ await device.load({ routerRtpCapabilities });
94
+ } catch (cause) {
95
+ throw new SfuError('device.load() failed', {
96
+ code: 'ZQ_WEBRTC_SFU_LOAD_FAILED',
97
+ cause,
98
+ });
99
+ }
100
+ },
101
+
102
+ /**
103
+ * @param {'audio'|'video'} kind
104
+ * @returns {boolean}
105
+ */
106
+ canProduce(kind) {
107
+ if (!device.loaded) {
108
+ throw new SfuError('canProduce(): device not loaded yet', {
109
+ code: 'ZQ_WEBRTC_SFU_NOT_LOADED',
110
+ });
111
+ }
112
+ return !!device.canProduce(kind);
113
+ },
114
+
115
+ /** @param {any} params */
116
+ createSendTransport(params) {
117
+ if (!device.loaded) {
118
+ throw new SfuError('createSendTransport(): device not loaded yet', {
119
+ code: 'ZQ_WEBRTC_SFU_NOT_LOADED',
120
+ });
121
+ }
122
+ return device.createSendTransport(params);
123
+ },
124
+
125
+ /** @param {any} params */
126
+ createRecvTransport(params) {
127
+ if (!device.loaded) {
128
+ throw new SfuError('createRecvTransport(): device not loaded yet', {
129
+ code: 'ZQ_WEBRTC_SFU_NOT_LOADED',
130
+ });
131
+ }
132
+ return device.createRecvTransport(params);
133
+ },
134
+
135
+ /**
136
+ * Reserved for the higher-level join flow once SFU-specific signaling
137
+ * is wired through `Room`. Today, callers should use the device and
138
+ * transport helpers above directly.
139
+ *
140
+ * @param {any} _joinOpts
141
+ * @returns {Promise<never>}
142
+ */
143
+ async join(_joinOpts) {
144
+ throw new SfuError(
145
+ 'mediasoup adapter.join() not implemented; use device + createSendTransport/createRecvTransport with your SFU signaling layer',
146
+ { code: 'ZQ_WEBRTC_SFU_JOIN_UNAVAILABLE' },
147
+ );
148
+ },
149
+ };
150
+ }