zero-query 1.1.1 → 1.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.
Files changed (156) 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 +442 -442
  5. package/cli/commands/build.js +253 -247
  6. package/cli/commands/bundle.js +1226 -1224
  7. package/cli/commands/create.js +178 -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 -220
  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 +441 -0
  73. package/cli/scaffold/webrtc/assets/.gitkeep +0 -0
  74. package/cli/scaffold/webrtc/global.css +250 -0
  75. package/cli/scaffold/webrtc/index.html +21 -0
  76. package/cli/scaffold/webrtc/package.json +16 -0
  77. package/cli/scaffold/webrtc/server/index.js +198 -0
  78. package/cli/utils.js +305 -287
  79. package/dist/API.md +661 -0
  80. package/dist/zquery.dist.zip +0 -0
  81. package/dist/zquery.js +10454 -6614
  82. package/dist/zquery.min.js +8 -631
  83. package/index.d.ts +570 -371
  84. package/index.js +311 -240
  85. package/package.json +77 -70
  86. package/src/component.js +1758 -1691
  87. package/src/core.js +929 -921
  88. package/src/diff.js +497 -497
  89. package/src/errors.js +209 -209
  90. package/src/expression.js +929 -922
  91. package/src/http.js +242 -242
  92. package/src/package.json +1 -1
  93. package/src/reactive.js +271 -255
  94. package/src/router.js +878 -843
  95. package/src/ssr.js +421 -418
  96. package/src/store.js +325 -318
  97. package/src/utils.js +534 -515
  98. package/src/webrtc/e2ee.js +351 -0
  99. package/src/webrtc/errors.js +116 -0
  100. package/src/webrtc/ice.js +301 -0
  101. package/src/webrtc/index.js +131 -0
  102. package/src/webrtc/joinToken.js +119 -0
  103. package/src/webrtc/observe.js +172 -0
  104. package/src/webrtc/peer.js +351 -0
  105. package/src/webrtc/reactive.js +268 -0
  106. package/src/webrtc/room.js +625 -0
  107. package/src/webrtc/sdp.js +302 -0
  108. package/src/webrtc/sfu/index.js +43 -0
  109. package/src/webrtc/sfu/livekit.js +131 -0
  110. package/src/webrtc/sfu/mediasoup.js +150 -0
  111. package/src/webrtc/signaling.js +373 -0
  112. package/src/webrtc/turn.js +237 -0
  113. package/tests/_helpers/webrtcFakes.js +289 -0
  114. package/tests/audit.test.js +4158 -4158
  115. package/tests/bench/render.bench.js +77 -0
  116. package/tests/cli.test.js +1136 -1103
  117. package/tests/compare.test.js +497 -486
  118. package/tests/component.test.js +4076 -3938
  119. package/tests/core.test.js +1926 -1910
  120. package/tests/dev-server.test.js +489 -489
  121. package/tests/diff.test.js +1416 -1416
  122. package/tests/docs.test.js +1664 -1650
  123. package/tests/electron-features.test.js +864 -864
  124. package/tests/errors.test.js +619 -619
  125. package/tests/expression.test.js +1087 -1056
  126. package/tests/http.test.js +693 -648
  127. package/tests/reactive.test.js +844 -819
  128. package/tests/router.test.js +2423 -2327
  129. package/tests/ssr.test.js +916 -870
  130. package/tests/store.test.js +840 -830
  131. package/tests/test-minifier.js +153 -153
  132. package/tests/test-ssr.js +27 -27
  133. package/tests/utils.test.js +1411 -1377
  134. package/tests/webrtc/e2ee.test.js +283 -0
  135. package/tests/webrtc/ice.test.js +202 -0
  136. package/tests/webrtc/joinToken.test.js +89 -0
  137. package/tests/webrtc/observe.test.js +111 -0
  138. package/tests/webrtc/peer.test.js +373 -0
  139. package/tests/webrtc/reactive.test.js +235 -0
  140. package/tests/webrtc/room.test.js +406 -0
  141. package/tests/webrtc/sdp.test.js +151 -0
  142. package/tests/webrtc/sfu-livekit.test.js +119 -0
  143. package/tests/webrtc/sfu.test.js +160 -0
  144. package/tests/webrtc/signaling.test.js +251 -0
  145. package/tests/webrtc/turn.test.js +256 -0
  146. package/types/collection.d.ts +383 -383
  147. package/types/component.d.ts +186 -186
  148. package/types/errors.d.ts +135 -135
  149. package/types/http.d.ts +92 -92
  150. package/types/misc.d.ts +201 -201
  151. package/types/reactive.d.ts +98 -98
  152. package/types/router.d.ts +190 -190
  153. package/types/ssr.d.ts +102 -102
  154. package/types/store.d.ts +146 -146
  155. package/types/utils.d.ts +245 -245
  156. 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
+ }