uidex 0.6.0 → 0.7.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 (39) hide show
  1. package/README.md +3 -3
  2. package/dist/cli/cli.cjs +1510 -1244
  3. package/dist/cli/cli.cjs.map +1 -1
  4. package/dist/cloud/index.cjs +385 -175
  5. package/dist/cloud/index.cjs.map +1 -1
  6. package/dist/cloud/index.d.cts +192 -4
  7. package/dist/cloud/index.d.ts +192 -4
  8. package/dist/cloud/index.js +377 -177
  9. package/dist/cloud/index.js.map +1 -1
  10. package/dist/headless/index.cjs +82 -255
  11. package/dist/headless/index.cjs.map +1 -1
  12. package/dist/headless/index.d.cts +5 -11
  13. package/dist/headless/index.d.ts +5 -11
  14. package/dist/headless/index.js +82 -257
  15. package/dist/headless/index.js.map +1 -1
  16. package/dist/index.cjs +721 -1053
  17. package/dist/index.cjs.map +1 -1
  18. package/dist/index.d.cts +149 -160
  19. package/dist/index.d.ts +149 -160
  20. package/dist/index.js +741 -1068
  21. package/dist/index.js.map +1 -1
  22. package/dist/react/index.cjs +729 -1000
  23. package/dist/react/index.cjs.map +1 -1
  24. package/dist/react/index.d.cts +99 -86
  25. package/dist/react/index.d.ts +99 -86
  26. package/dist/react/index.js +745 -1015
  27. package/dist/react/index.js.map +1 -1
  28. package/dist/scan/index.cjs +1518 -1237
  29. package/dist/scan/index.cjs.map +1 -1
  30. package/dist/scan/index.d.cts +209 -12
  31. package/dist/scan/index.d.ts +209 -12
  32. package/dist/scan/index.js +1515 -1236
  33. package/dist/scan/index.js.map +1 -1
  34. package/package.json +22 -21
  35. package/templates/claude/SKILL.md +71 -0
  36. package/templates/claude/references/audit.md +43 -0
  37. package/templates/claude/{rules.md → references/conventions.md} +25 -28
  38. package/templates/claude/audit.md +0 -43
  39. /package/templates/claude/{api.md → references/api.md} +0 -0
@@ -1,91 +1,46 @@
1
- // src/cloud/client.ts
2
- import { createClient as createClient2, createConfig as createConfig2 } from "@hey-api/client-fetch";
3
-
4
- // ../api-client/src/client.gen.ts
5
- import {
6
- createClient,
7
- createConfig
8
- } from "@hey-api/client-fetch";
9
- var client = createClient(
10
- createConfig({
11
- baseUrl: "https://app.uidex.dev"
12
- })
13
- );
14
-
15
- // ../api-client/src/sdk.gen.ts
16
- var submitReport = (options) => {
17
- return (options.client ?? client).post({
18
- security: [
19
- {
20
- scheme: "bearer",
21
- type: "http"
22
- }
23
- ],
24
- url: "/api/ingest",
25
- ...options,
26
- headers: {
27
- "Content-Type": "application/json",
28
- ...options?.headers
29
- }
30
- });
31
- };
32
- var getIngestConfig = (options) => {
33
- return (options?.client ?? client).get({
34
- security: [
35
- {
36
- scheme: "bearer",
37
- type: "http"
38
- }
39
- ],
40
- url: "/api/ingest/config",
41
- ...options
42
- });
43
- };
44
- var listIngestReports = (options) => {
45
- return (options?.client ?? client).get({
46
- security: [
47
- {
48
- scheme: "bearer",
49
- type: "http"
50
- }
51
- ],
52
- url: "/api/ingest/reports",
53
- ...options
54
- });
55
- };
56
- var listPins = (options) => {
57
- return (options?.client ?? client).get({
58
- security: [
59
- {
60
- scheme: "bearer",
61
- type: "http"
62
- }
63
- ],
64
- url: "/api/ingest/pins",
65
- ...options
66
- });
67
- };
68
- var archivePin = (options) => {
69
- return (options.client ?? client).post({
70
- security: [
71
- {
72
- scheme: "bearer",
73
- type: "http"
74
- }
75
- ],
76
- url: "/api/ingest/pins/archive",
77
- ...options,
78
- headers: {
79
- "Content-Type": "application/json",
80
- ...options?.headers
81
- }
82
- });
83
- };
1
+ // src/cloud/protocol.ts
2
+ var RPC_METHODS = [
3
+ "reports.submit",
4
+ "reports.list",
5
+ "config.get",
6
+ "pins.list",
7
+ "pins.screenshot",
8
+ "pins.archive",
9
+ "screenshot.chunk"
10
+ ];
11
+ var SCREENSHOT_INLINE_MAX_BYTES = 20 * 1024 * 1024;
12
+ var SCREENSHOT_CHUNK_BYTES = 8 * 1024 * 1024;
13
+ function isRpcMethod(value) {
14
+ return typeof value === "string" && RPC_METHODS.includes(value);
15
+ }
16
+ function parseRpcResponseFrame(msg) {
17
+ if (!msg || typeof msg !== "object") return null;
18
+ const m = msg;
19
+ if (m.type !== "rpc:res" || typeof m.id !== "string") return null;
20
+ if (m.ok === true) {
21
+ return { type: "rpc:res", id: m.id, ok: true, data: m.data };
22
+ }
23
+ if (m.ok === false) {
24
+ if (typeof m.status !== "number" || typeof m.error !== "string") return null;
25
+ return {
26
+ type: "rpc:res",
27
+ id: m.id,
28
+ ok: false,
29
+ status: m.status,
30
+ error: m.error,
31
+ retryAfter: typeof m.retryAfter === "number" ? m.retryAfter : void 0,
32
+ details: m.details
33
+ };
34
+ }
35
+ return null;
36
+ }
84
37
 
85
38
  // src/cloud/realtime.ts
86
39
  var RECONNECT_INITIAL_MS = 1e3;
87
40
  var RECONNECT_MAX_MS = 3e4;
41
+ var MAX_RECONNECT_ATTEMPTS = 10;
88
42
  var CLOSE_CODE_AUTH_FAILED = 4001;
43
+ var CLOSE_CODE_SYNTHETIC = 1006;
89
44
  function emit(listeners, value) {
90
45
  for (const cb of listeners) {
91
46
  try {
@@ -104,14 +59,28 @@ function resolveWebSocket(override) {
104
59
  );
105
60
  }
106
61
  function createRealtimeChannel(options) {
107
- const WS = resolveWebSocket(options.WebSocketImpl);
62
+ let WS = options.WebSocketImpl ?? null;
108
63
  const presenceListeners = /* @__PURE__ */ new Set();
109
64
  const pinListeners = /* @__PURE__ */ new Set();
65
+ const pinArchivedListeners = /* @__PURE__ */ new Set();
66
+ const openListeners = /* @__PURE__ */ new Set();
67
+ const closeListeners = /* @__PURE__ */ new Set();
68
+ const rpcListeners = /* @__PURE__ */ new Set();
110
69
  let ws = null;
111
70
  let state = "disconnected";
112
71
  let disposed = false;
113
72
  let reconnectAttempts = 0;
114
73
  let reconnectTimer = null;
74
+ let identity = null;
75
+ let route = null;
76
+ function emitClose(code, willReconnect) {
77
+ for (const cb of closeListeners) {
78
+ try {
79
+ cb(code, willReconnect);
80
+ } catch {
81
+ }
82
+ }
83
+ }
115
84
  function clearReconnectTimer() {
116
85
  if (reconnectTimer !== null) {
117
86
  clearTimeout(reconnectTimer);
@@ -142,6 +111,11 @@ function createRealtimeChannel(options) {
142
111
  }
143
112
  if (!parsed || typeof parsed !== "object") return;
144
113
  const msg = parsed;
114
+ if (msg.type === "rpc:res") {
115
+ const frame = parseRpcResponseFrame(parsed);
116
+ if (frame) emit(rpcListeners, frame);
117
+ return;
118
+ }
145
119
  if (msg.type === "presence") {
146
120
  const p = msg;
147
121
  if (!Array.isArray(p.users)) return;
@@ -159,15 +133,21 @@ function createRealtimeChannel(options) {
159
133
  emit(presenceListeners, users);
160
134
  return;
161
135
  }
136
+ if (msg.type === "pin:archived") {
137
+ const p = msg;
138
+ if (typeof p.reportId !== "string") return;
139
+ emit(pinArchivedListeners, p.reportId);
140
+ return;
141
+ }
162
142
  if (msg.type === "pin") {
163
143
  const p = msg;
164
- if (typeof p.feedbackId !== "string" || !p.elementRef || typeof p.elementRef !== "object" || typeof p.elementRef.kind !== "string" || typeof p.elementRef.id !== "string" || !p.author || typeof p.author !== "object" || typeof p.body !== "string" || typeof p.reportType !== "string" || typeof p.reportSeverity !== "string" || typeof p.createdAt !== "string") {
144
+ if (typeof p.reportId !== "string" || !p.elementRef || typeof p.elementRef !== "object" || typeof p.elementRef.kind !== "string" || typeof p.elementRef.id !== "string" || !p.author || typeof p.author !== "object" || typeof p.body !== "string" || typeof p.reportType !== "string" || typeof p.reportSeverity !== "string" || typeof p.createdAt !== "string") {
165
145
  return;
166
146
  }
167
147
  const author = p.author;
168
148
  const elRef = p.elementRef;
169
149
  const pin = {
170
- id: p.feedbackId,
150
+ id: p.reportId,
171
151
  entity: `${elRef.kind}:${elRef.id}`,
172
152
  reporter: {
173
153
  name: typeof author.name === "string" ? author.name : void 0,
@@ -184,20 +164,45 @@ function createRealtimeChannel(options) {
184
164
  return;
185
165
  }
186
166
  }
167
+ function sendRaw(frame) {
168
+ if (!ws || !WS || ws.readyState !== WS.OPEN) return false;
169
+ try {
170
+ ws.send(JSON.stringify(frame));
171
+ return true;
172
+ } catch {
173
+ return false;
174
+ }
175
+ }
187
176
  function openSocket() {
188
177
  if (disposed) return;
189
178
  state = "connecting";
179
+ if (!WS) {
180
+ try {
181
+ WS = resolveWebSocket(options.WebSocketImpl);
182
+ } catch {
183
+ state = "disconnected";
184
+ emitClose(CLOSE_CODE_SYNTHETIC, false);
185
+ return;
186
+ }
187
+ }
190
188
  let url;
191
189
  try {
192
190
  url = options.buildUrl();
193
191
  } catch {
194
192
  state = "disconnected";
193
+ emitClose(CLOSE_CODE_SYNTHETIC, false);
195
194
  return;
196
195
  }
197
196
  let socket;
198
197
  try {
199
198
  socket = new WS(url);
200
199
  } catch {
200
+ if (reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
201
+ state = "disconnected";
202
+ emitClose(CLOSE_CODE_SYNTHETIC, false);
203
+ return;
204
+ }
205
+ emitClose(CLOSE_CODE_SYNTHETIC, true);
201
206
  scheduleReconnect();
202
207
  return;
203
208
  }
@@ -206,6 +211,18 @@ function createRealtimeChannel(options) {
206
211
  if (ws !== socket) return;
207
212
  state = "connected";
208
213
  reconnectAttempts = 0;
214
+ if (identity) {
215
+ sendRaw({
216
+ type: "identify",
217
+ userId: identity.id,
218
+ ...identity.name ? { name: identity.name } : {},
219
+ ...identity.avatar ? { avatar: identity.avatar } : {}
220
+ });
221
+ }
222
+ if (route !== null) {
223
+ sendRaw({ type: "join", route });
224
+ }
225
+ emit(openListeners, void 0);
209
226
  });
210
227
  socket.addEventListener("message", (event) => {
211
228
  if (ws !== socket) return;
@@ -215,10 +232,12 @@ function createRealtimeChannel(options) {
215
232
  if (ws !== socket) return;
216
233
  ws = null;
217
234
  const code = event.code;
218
- if (disposed || code === CLOSE_CODE_AUTH_FAILED) {
235
+ if (disposed || code === CLOSE_CODE_AUTH_FAILED || reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
219
236
  state = "disconnected";
237
+ emitClose(code, false);
220
238
  return;
221
239
  }
240
+ emitClose(code, true);
222
241
  scheduleReconnect();
223
242
  });
224
243
  socket.addEventListener("error", () => {
@@ -237,22 +256,35 @@ function createRealtimeChannel(options) {
237
256
  disposed = true;
238
257
  clearReconnectTimer();
239
258
  state = "disconnected";
259
+ identity = null;
260
+ route = null;
240
261
  if (ws) {
262
+ const socket = ws;
263
+ ws = null;
241
264
  try {
242
- ws.close(1e3);
265
+ socket.close(1e3);
243
266
  } catch {
244
267
  }
245
- ws = null;
246
268
  }
269
+ emitClose(CLOSE_CODE_SYNTHETIC, false);
247
270
  }
248
- function joinRoute(route) {
271
+ function clearSession() {
272
+ identity = null;
273
+ route = null;
274
+ }
275
+ function identify(user) {
276
+ identity = user;
277
+ sendRaw({
278
+ type: "identify",
279
+ userId: user.id,
280
+ ...user.name ? { name: user.name } : {},
281
+ ...user.avatar ? { avatar: user.avatar } : {}
282
+ });
283
+ }
284
+ function joinRoute(nextRoute) {
285
+ route = nextRoute;
249
286
  emit(presenceListeners, []);
250
- if (ws && ws.readyState === WS.OPEN) {
251
- try {
252
- ws.send(JSON.stringify({ type: "join", route }));
253
- } catch {
254
- }
255
- }
287
+ sendRaw({ type: "join", route: nextRoute });
256
288
  }
257
289
  function onPresence(cb) {
258
290
  presenceListeners.add(cb);
@@ -266,15 +298,46 @@ function createRealtimeChannel(options) {
266
298
  pinListeners.delete(cb);
267
299
  };
268
300
  }
301
+ function onPinArchived(cb) {
302
+ pinArchivedListeners.add(cb);
303
+ return () => {
304
+ pinArchivedListeners.delete(cb);
305
+ };
306
+ }
307
+ function onOpen(cb) {
308
+ openListeners.add(cb);
309
+ return () => {
310
+ openListeners.delete(cb);
311
+ };
312
+ }
313
+ function onClose(cb) {
314
+ closeListeners.add(cb);
315
+ return () => {
316
+ closeListeners.delete(cb);
317
+ };
318
+ }
319
+ function onRpcResponse(cb) {
320
+ rpcListeners.add(cb);
321
+ return () => {
322
+ rpcListeners.delete(cb);
323
+ };
324
+ }
269
325
  return {
270
326
  get state() {
271
327
  return state;
272
328
  },
273
329
  connect,
274
330
  disconnect,
331
+ identify,
332
+ clearSession,
275
333
  joinRoute,
334
+ sendFrame: sendRaw,
276
335
  onPresence,
277
- onPin
336
+ onPin,
337
+ onPinArchived,
338
+ onOpen,
339
+ onClose,
340
+ onRpcResponse
278
341
  };
279
342
  }
280
343
 
@@ -293,29 +356,111 @@ var CloudError = class extends Error {
293
356
  }
294
357
  };
295
358
 
359
+ // src/cloud/rpc.ts
360
+ var DEFAULT_RPC_TIMEOUT_MS = 3e4;
361
+ var SUBMIT_RPC_TIMEOUT_MS = 12e4;
362
+ function authFailedError() {
363
+ return new CloudError("WebSocket authentication failed", { status: 401 });
364
+ }
365
+ function createRpcClient(channel, options) {
366
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_RPC_TIMEOUT_MS;
367
+ const idSuffix = Math.random().toString(36).slice(2, 8);
368
+ let idCounter = 0;
369
+ const pending = /* @__PURE__ */ new Map();
370
+ const queue = [];
371
+ let terminalError = null;
372
+ function settlePending(err) {
373
+ for (const entry of pending.values()) {
374
+ clearTimeout(entry.timer);
375
+ entry.reject(err);
376
+ }
377
+ pending.clear();
378
+ }
379
+ channel.onOpen(() => {
380
+ while (queue.length > 0) {
381
+ const next = queue[0];
382
+ if (!channel.sendFrame(next.frame)) return;
383
+ queue.shift();
384
+ pending.set(next.frame.id, next.entry);
385
+ }
386
+ });
387
+ function rejectQueue(err) {
388
+ for (const queued of queue) {
389
+ clearTimeout(queued.entry.timer);
390
+ queued.entry.reject(err);
391
+ }
392
+ queue.length = 0;
393
+ }
394
+ channel.onClose((code, willReconnect) => {
395
+ if (code === CLOSE_CODE_AUTH_FAILED) {
396
+ const err = authFailedError();
397
+ terminalError = err;
398
+ settlePending(err);
399
+ rejectQueue(err);
400
+ return;
401
+ }
402
+ settlePending(new CloudError("Connection lost", { status: 0 }));
403
+ if (!willReconnect) {
404
+ rejectQueue(new CloudError("Connection lost", { status: 0 }));
405
+ }
406
+ });
407
+ channel.onRpcResponse((frame) => {
408
+ const entry = pending.get(frame.id);
409
+ if (!entry) return;
410
+ pending.delete(frame.id);
411
+ clearTimeout(entry.timer);
412
+ if (frame.ok) {
413
+ entry.resolve(frame.data);
414
+ } else {
415
+ entry.reject(
416
+ new CloudError(frame.error, {
417
+ status: frame.status,
418
+ retryAfter: frame.retryAfter,
419
+ details: frame.details
420
+ })
421
+ );
422
+ }
423
+ });
424
+ function call(method, params, opts) {
425
+ if (terminalError) return Promise.reject(terminalError);
426
+ const callTimeoutMs = opts?.timeoutMs ?? timeoutMs;
427
+ return new Promise((resolve, reject) => {
428
+ idCounter += 1;
429
+ const id = `${idCounter.toString(36)}-${idSuffix}`;
430
+ const frame = {
431
+ type: "rpc",
432
+ id,
433
+ method,
434
+ ...params !== void 0 ? { params } : {}
435
+ };
436
+ const timer = setTimeout(() => {
437
+ pending.delete(id);
438
+ const queuedIndex = queue.findIndex((q) => q.frame.id === id);
439
+ if (queuedIndex !== -1) queue.splice(queuedIndex, 1);
440
+ reject(new CloudError("Request timed out", { status: 0 }));
441
+ }, callTimeoutMs);
442
+ const entry = {
443
+ resolve,
444
+ reject,
445
+ timer
446
+ };
447
+ if (channel.sendFrame(frame)) {
448
+ pending.set(id, entry);
449
+ } else {
450
+ queue.push({ frame, entry });
451
+ channel.connect();
452
+ }
453
+ });
454
+ }
455
+ return { call };
456
+ }
457
+
296
458
  // src/cloud/client.ts
297
459
  function trimEndpoint(endpoint) {
298
460
  return endpoint.endsWith("/") ? endpoint.slice(0, -1) : endpoint;
299
461
  }
300
- function unwrap(result) {
301
- if (result.error !== void 0 || !result.data) {
302
- const status = result.response.status;
303
- const retryAfter = status === 429 ? parseRetryAfter(result.response.headers.get("Retry-After")) : void 0;
304
- const msg = result.error && typeof result.error === "object" && "error" in result.error && typeof result.error.error === "string" ? result.error.error : `Request failed (${status})`;
305
- throw new CloudError(msg, { status, retryAfter, details: result.error });
306
- }
307
- return result.data;
308
- }
309
- function parseRetryAfter(header) {
310
- if (!header) return void 0;
311
- const seconds = Number(header);
312
- if (Number.isFinite(seconds) && seconds >= 0) return seconds;
313
- const date = Date.parse(header);
314
- if (Number.isFinite(date)) {
315
- const delta = Math.ceil((date - Date.now()) / 1e3);
316
- return delta > 0 ? delta : 0;
317
- }
318
- return void 0;
462
+ function nextUploadId() {
463
+ return `up-${crypto.randomUUID()}`;
319
464
  }
320
465
  function cloud(options) {
321
466
  let cachedConfig = null;
@@ -326,13 +471,18 @@ function cloud(options) {
326
471
  }
327
472
  const endpoint = trimEndpoint(options.endpoint ?? DEFAULT_CLOUD_ENDPOINT);
328
473
  const git = options.git;
329
- const apiClient = createClient2(
330
- createConfig2({
331
- baseUrl: endpoint,
332
- ...options.fetch ? { fetch: options.fetch } : {},
333
- headers: { Authorization: `Bearer ${projectKey}` }
334
- })
335
- );
474
+ function buildWsUrl() {
475
+ const httpToWs = endpoint.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
476
+ const params = new URLSearchParams();
477
+ params.set("key", projectKey);
478
+ return `${httpToWs}/ws?${params.toString()}`;
479
+ }
480
+ const channel = createRealtimeChannel({
481
+ buildUrl: buildWsUrl,
482
+ ...options.WebSocketImpl ? { WebSocketImpl: options.WebSocketImpl } : {}
483
+ });
484
+ const rpc = createRpcClient(channel);
485
+ const eagerConnect = options.WebSocketImpl !== void 0 || typeof window !== "undefined" && typeof WebSocket === "function";
336
486
  async function submit(payload) {
337
487
  const enriched = git?.branch || git?.commit ? {
338
488
  ...payload,
@@ -344,102 +494,152 @@ function cloud(options) {
344
494
  }
345
495
  }
346
496
  } : payload;
347
- const result = await submitReport({
348
- client: apiClient,
349
- body: enriched
497
+ const screenshot = enriched.screenshot;
498
+ if (typeof screenshot === "string" && screenshot.length > SCREENSHOT_INLINE_MAX_BYTES) {
499
+ const uploadId = nextUploadId();
500
+ const total = Math.ceil(screenshot.length / SCREENSHOT_CHUNK_BYTES);
501
+ for (let seq = 0; seq < total; seq++) {
502
+ const data = screenshot.slice(
503
+ seq * SCREENSHOT_CHUNK_BYTES,
504
+ (seq + 1) * SCREENSHOT_CHUNK_BYTES
505
+ );
506
+ await rpc.call(
507
+ "screenshot.chunk",
508
+ { uploadId, seq, total, data },
509
+ { timeoutMs: SUBMIT_RPC_TIMEOUT_MS }
510
+ );
511
+ }
512
+ return rpc.call(
513
+ "reports.submit",
514
+ { ...enriched, screenshot: void 0, screenshotUploadId: uploadId },
515
+ { timeoutMs: SUBMIT_RPC_TIMEOUT_MS }
516
+ );
517
+ }
518
+ return rpc.call("reports.submit", enriched, {
519
+ timeoutMs: SUBMIT_RPC_TIMEOUT_MS
350
520
  });
351
- return unwrap(result);
352
- }
353
- async function fetchConfig() {
354
- const result = await getIngestConfig({ client: apiClient });
355
- return unwrap(result);
356
521
  }
357
522
  function startFetch() {
358
- const promise = fetchConfig();
523
+ const promise = rpc.call("config.get", void 0);
524
+ cachedConfig = promise;
359
525
  promise.then(
360
526
  (config) => {
361
527
  resolvedConfig = config;
362
528
  },
363
529
  () => {
530
+ if (cachedConfig === promise) cachedConfig = null;
364
531
  }
365
532
  );
366
533
  return promise;
367
534
  }
368
- cachedConfig = startFetch();
535
+ if (eagerConnect) {
536
+ channel.connect();
537
+ startFetch();
538
+ }
369
539
  function getConfig() {
370
- return cachedConfig ?? (cachedConfig = startFetch());
540
+ return cachedConfig ?? startFetch();
371
541
  }
372
542
  function getCachedConfig() {
373
543
  return resolvedConfig;
374
544
  }
375
- function realtimeUrl(route, user) {
376
- const httpToWs = endpoint.replace(/^http:/, "ws:").replace(/^https:/, "wss:");
377
- const params = new URLSearchParams();
378
- params.set("key", projectKey);
379
- params.set("route", route);
380
- params.set("userId", user.id);
381
- if (user.name) params.set("name", user.name);
382
- if (user.avatar) params.set("avatar", user.avatar);
383
- return `${httpToWs}/ws?${params.toString()}`;
384
- }
385
545
  function connectRealtime(opts) {
386
546
  if (!opts || !opts.user || typeof opts.user.id !== "string") {
387
547
  throw new TypeError("uidex/cloud: realtime.connect requires `user.id`");
388
548
  }
389
549
  let currentRoute = opts.route;
390
- const channel = createRealtimeChannel({
391
- buildUrl: () => realtimeUrl(currentRoute, opts.user)
392
- });
393
- const originalJoinRoute = channel.joinRoute;
394
- const wrapped = {
550
+ let detached = false;
551
+ const registrations = /* @__PURE__ */ new Set();
552
+ function register(attach) {
553
+ const reg = { attach, detach: detached ? null : attach() };
554
+ registrations.add(reg);
555
+ return () => {
556
+ registrations.delete(reg);
557
+ reg.detach?.();
558
+ reg.detach = null;
559
+ };
560
+ }
561
+ function arm() {
562
+ channel.identify(opts.user);
563
+ channel.joinRoute(currentRoute);
564
+ channel.connect();
565
+ }
566
+ arm();
567
+ return {
395
568
  get state() {
396
- return channel.state;
569
+ return detached ? "disconnected" : channel.state;
570
+ },
571
+ // Re-arms identity/route and re-attaches this facade's listeners so an
572
+ // explicit disconnect() -> connect() restores presence and pin events.
573
+ connect: () => {
574
+ if (detached) {
575
+ detached = false;
576
+ for (const reg of registrations) reg.detach ??= reg.attach();
577
+ }
578
+ arm();
579
+ },
580
+ // Soft leave: drop presence and the stored identity (so a later
581
+ // reopen/revival stays anonymous) but keep the adapter's shared socket
582
+ // alive — in-flight and future RPCs (reports.submit, pins.*) must
583
+ // survive a surface unmount. Socket teardown is reserved for
584
+ // CloudAdapter.dispose().
585
+ disconnect: () => {
586
+ detached = true;
587
+ channel.sendFrame({ type: "leave" });
588
+ channel.clearSession();
589
+ for (const reg of registrations) {
590
+ reg.detach?.();
591
+ reg.detach = null;
592
+ }
397
593
  },
398
- connect: () => channel.connect(),
399
- disconnect: () => channel.disconnect(),
400
594
  joinRoute: (route) => {
401
595
  currentRoute = route;
402
- originalJoinRoute(route);
596
+ channel.joinRoute(route);
403
597
  },
404
- onPresence: (cb) => channel.onPresence(cb),
405
- onPin: (cb) => channel.onPin(cb)
598
+ onPresence: (cb) => register(() => channel.onPresence(cb)),
599
+ onPin: (cb) => register(() => channel.onPin(cb)),
600
+ onPinArchived: (cb) => register(() => channel.onPinArchived(cb))
406
601
  };
407
- channel.connect();
408
- return wrapped;
409
602
  }
410
- async function listPins2(params) {
411
- const result = await listPins({
412
- client: apiClient,
413
- query: params
603
+ async function listPins(params) {
604
+ const entities = params.entities ? params.entities.split(",").filter(Boolean) : void 0;
605
+ const result = await rpc.call("pins.list", {
606
+ ...params.route !== void 0 ? { route: params.route } : {},
607
+ ...entities ? { entities } : {}
414
608
  });
415
- const data = unwrap(result);
416
- return data.pins;
609
+ return result.pins;
417
610
  }
418
- async function archivePin2(reportId, reason) {
419
- const result = await archivePin({
420
- client: apiClient,
421
- body: { reportId, ...reason ? { reason } : {} }
422
- });
423
- unwrap(result);
611
+ async function getPinScreenshot(reportId) {
612
+ const result = await rpc.call("pins.screenshot", { reportId });
613
+ return result.screenshot;
424
614
  }
425
- async function listReports(opts) {
426
- const result = await listIngestReports({
427
- client: apiClient,
428
- query: opts
429
- });
430
- return unwrap(result);
615
+ async function closePin(reportId, reason) {
616
+ await rpc.call("pins.archive", { reportId, ...reason ? { reason } : {} });
617
+ }
618
+ function listReports(opts) {
619
+ return rpc.call("reports.list", opts ?? {});
620
+ }
621
+ function dispose() {
622
+ channel.disconnect();
431
623
  }
432
624
  return {
433
625
  reports: { submit, list: listReports },
434
626
  integrations: { getConfig, getCachedConfig },
435
627
  realtime: { connect: connectRealtime },
436
- pins: { list: listPins2, archive: archivePin2 }
628
+ pins: { list: listPins, screenshot: getPinScreenshot, close: closePin },
629
+ dispose
437
630
  };
438
631
  }
439
632
  export {
440
633
  CloudError,
441
634
  DEFAULT_CLOUD_ENDPOINT,
635
+ DEFAULT_RPC_TIMEOUT_MS,
636
+ RPC_METHODS,
637
+ SCREENSHOT_CHUNK_BYTES,
638
+ SCREENSHOT_INLINE_MAX_BYTES,
442
639
  cloud,
443
- createRealtimeChannel
640
+ createRealtimeChannel,
641
+ createRpcClient,
642
+ isRpcMethod,
643
+ parseRpcResponseFrame
444
644
  };
445
645
  //# sourceMappingURL=index.js.map