@ai-matrx/desktop-protocol 0.1.0 → 0.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.
package/dist/client.d.cts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { BackoffOptions } from '@ai-matrx/realtime';
2
2
  export { RECONNECT_ALARM_ATTEMPTS } from '@ai-matrx/realtime';
3
3
  import { z } from 'zod';
4
- import { W as Welcome, D as DesktopProtocolError, O as OpName, a as OpParams, b as OpResult, c as OPS, S as StreamMeta, E as EndReason, d as EVENTS, C as ClientType } from './registry-DTcTGycI.cjs';
5
- export { i as isDesktopProtocolError } from './registry-DTcTGycI.cjs';
4
+ import { W as Welcome, D as DesktopProtocolError, O as OpName, a as OpParams, b as OpResult, c as OPS, S as StreamMeta, E as EndReason, d as EVENTS, C as ClientType } from './registry-DJbrsZ4u.cjs';
5
+ export { i as isDesktopProtocolError } from './registry-DJbrsZ4u.cjs';
6
6
  import { FrameHeader } from './frame.cjs';
7
7
 
8
8
  /** The subset of the WHATWG WebSocket the client uses — browser, React Native, `ws` and Workers all fit. */
@@ -167,6 +167,18 @@ interface DesktopClient {
167
167
  on<E extends EventName>(name: E, handler: EventHandler<E>): () => void;
168
168
  /** Hand the relay a fresh token without dropping the socket. */
169
169
  reauth(token: string): void;
170
+ /**
171
+ * The app came back to the foreground or the network changed (`visibilitychange` → visible,
172
+ * `pageshow`, `online`). Reconnecting → dial now instead of waiting out the backoff. Open → probe:
173
+ * a ping must be answered within `probeMs` (default 4000) or the socket is treated as dead and
174
+ * redialed at once. A phone resumed from sleep often holds a socket the OS already killed, and two
175
+ * silent heartbeats would cost 50 s. Resource streams reattach with `since_seq` as usual.
176
+ */
177
+ wake(options?: WakeOptions): void;
178
+ }
179
+ interface WakeOptions {
180
+ /** How long an open socket has to answer the probe ping. Default 4000. */
181
+ probeMs?: number;
170
182
  }
171
183
 
172
184
  /**
@@ -178,4 +190,20 @@ interface DesktopClient {
178
190
 
179
191
  declare function createDesktopClient(options: DesktopClientOptions): DesktopClient;
180
192
 
181
- export { type ClientTimers, type DesktopClient, type DesktopClientDiagnostic, type DesktopClientOptions, type DesktopClientState, type DesktopClientStatus, DesktopProtocolError, type DesktopStream, type EventHandler, type EventName, type EventPayload, OpName, OpParams, OpResult, type RequestOptions, type StreamHandlers, type StreamOpName, type StreamOptions, type WebSocketFactory, type WebSocketLike, createDesktopClient };
193
+ /**
194
+ * Wire a client's wake() to the browser events that mean "the person is back": the page becomes
195
+ * visible again (iOS Safari suspends a background tab's sockets), a back/forward-cache restore
196
+ * (`pageshow` with persisted), and the network returning (`online`). Framework-free; the React
197
+ * binding is `useDesktopWake` in `./react`.
198
+ */
199
+
200
+ /** The subset of window/document the binding listens on — injectable for tests and non-DOM hosts. */
201
+ interface WakeTargets {
202
+ window: Pick<Window, "addEventListener" | "removeEventListener">;
203
+ document: Pick<Document, "addEventListener" | "removeEventListener" | "visibilityState">;
204
+ }
205
+ declare function bindDesktopWake(client: Pick<DesktopClient, "wake">, options?: WakeOptions & {
206
+ targets?: WakeTargets;
207
+ }): () => void;
208
+
209
+ export { type ClientTimers, type DesktopClient, type DesktopClientDiagnostic, type DesktopClientOptions, type DesktopClientState, type DesktopClientStatus, DesktopProtocolError, type DesktopStream, type EventHandler, type EventName, type EventPayload, OpName, OpParams, OpResult, type RequestOptions, type StreamHandlers, type StreamOpName, type StreamOptions, type WakeOptions, type WakeTargets, type WebSocketFactory, type WebSocketLike, bindDesktopWake, createDesktopClient };
package/dist/client.d.ts CHANGED
@@ -1,8 +1,8 @@
1
1
  import { BackoffOptions } from '@ai-matrx/realtime';
2
2
  export { RECONNECT_ALARM_ATTEMPTS } from '@ai-matrx/realtime';
3
3
  import { z } from 'zod';
4
- import { W as Welcome, D as DesktopProtocolError, O as OpName, a as OpParams, b as OpResult, c as OPS, S as StreamMeta, E as EndReason, d as EVENTS, C as ClientType } from './registry-DTcTGycI.js';
5
- export { i as isDesktopProtocolError } from './registry-DTcTGycI.js';
4
+ import { W as Welcome, D as DesktopProtocolError, O as OpName, a as OpParams, b as OpResult, c as OPS, S as StreamMeta, E as EndReason, d as EVENTS, C as ClientType } from './registry-DJbrsZ4u.js';
5
+ export { i as isDesktopProtocolError } from './registry-DJbrsZ4u.js';
6
6
  import { FrameHeader } from './frame.js';
7
7
 
8
8
  /** The subset of the WHATWG WebSocket the client uses — browser, React Native, `ws` and Workers all fit. */
@@ -167,6 +167,18 @@ interface DesktopClient {
167
167
  on<E extends EventName>(name: E, handler: EventHandler<E>): () => void;
168
168
  /** Hand the relay a fresh token without dropping the socket. */
169
169
  reauth(token: string): void;
170
+ /**
171
+ * The app came back to the foreground or the network changed (`visibilitychange` → visible,
172
+ * `pageshow`, `online`). Reconnecting → dial now instead of waiting out the backoff. Open → probe:
173
+ * a ping must be answered within `probeMs` (default 4000) or the socket is treated as dead and
174
+ * redialed at once. A phone resumed from sleep often holds a socket the OS already killed, and two
175
+ * silent heartbeats would cost 50 s. Resource streams reattach with `since_seq` as usual.
176
+ */
177
+ wake(options?: WakeOptions): void;
178
+ }
179
+ interface WakeOptions {
180
+ /** How long an open socket has to answer the probe ping. Default 4000. */
181
+ probeMs?: number;
170
182
  }
171
183
 
172
184
  /**
@@ -178,4 +190,20 @@ interface DesktopClient {
178
190
 
179
191
  declare function createDesktopClient(options: DesktopClientOptions): DesktopClient;
180
192
 
181
- export { type ClientTimers, type DesktopClient, type DesktopClientDiagnostic, type DesktopClientOptions, type DesktopClientState, type DesktopClientStatus, DesktopProtocolError, type DesktopStream, type EventHandler, type EventName, type EventPayload, OpName, OpParams, OpResult, type RequestOptions, type StreamHandlers, type StreamOpName, type StreamOptions, type WebSocketFactory, type WebSocketLike, createDesktopClient };
193
+ /**
194
+ * Wire a client's wake() to the browser events that mean "the person is back": the page becomes
195
+ * visible again (iOS Safari suspends a background tab's sockets), a back/forward-cache restore
196
+ * (`pageshow` with persisted), and the network returning (`online`). Framework-free; the React
197
+ * binding is `useDesktopWake` in `./react`.
198
+ */
199
+
200
+ /** The subset of window/document the binding listens on — injectable for tests and non-DOM hosts. */
201
+ interface WakeTargets {
202
+ window: Pick<Window, "addEventListener" | "removeEventListener">;
203
+ document: Pick<Document, "addEventListener" | "removeEventListener" | "visibilityState">;
204
+ }
205
+ declare function bindDesktopWake(client: Pick<DesktopClient, "wake">, options?: WakeOptions & {
206
+ targets?: WakeTargets;
207
+ }): () => void;
208
+
209
+ export { type ClientTimers, type DesktopClient, type DesktopClientDiagnostic, type DesktopClientOptions, type DesktopClientState, type DesktopClientStatus, DesktopProtocolError, type DesktopStream, type EventHandler, type EventName, type EventPayload, OpName, OpParams, OpResult, type RequestOptions, type StreamHandlers, type StreamOpName, type StreamOptions, type WakeOptions, type WakeTargets, type WebSocketFactory, type WebSocketLike, bindDesktopWake, createDesktopClient };
package/dist/client.js CHANGED
@@ -254,7 +254,15 @@ var CoreToClientMsg = z3.discriminatedUnion("type", [ResponseMsg, ErrorMsg, Stre
254
254
  var CloseCode = {
255
255
  NORMAL: 1e3,
256
256
  GOING_AWAY: 1001,
257
- // relay deploy / app quit — reconnect immediately
257
+ // app quit; relay: the device was silent for 75 s — reconnect
258
+ /** Never sent by anyone: what a socket reports when it drops without a close frame. A relay
259
+ * deploy (every Durable Object restarts) arrives as this — reconnect immediately. */
260
+ ABNORMAL: 1006,
261
+ /** Relay → clients: the device (re)connected, so the core session they spoke to is gone —
262
+ * reconnect, hello, and reattach with since_seq. */
263
+ DEVICE_RECONNECTED: 1012,
264
+ TRY_AGAIN_LATER: 1013,
265
+ // relay: signing keys or the device registry unreachable — retry
258
266
  AUTH_INVALID: 4001,
259
267
  AUTH_FORBIDDEN: 4003,
260
268
  DEVICE_NOT_FOUND: 4004,
@@ -267,6 +275,7 @@ var CloseCode = {
267
275
  PROTOCOL_ERROR: 4400,
268
276
  UPGRADE_REQUIRED: 4426
269
277
  };
278
+ var CloseCodeValue = z3.literal([1e3, 1001, 1006, 1012, 1013, 4001, 4003, 4004, 4008, 4009, 4010, 4400, 4426]);
270
279
 
271
280
  // src/schema/frame.ts
272
281
  import { z as z4 } from "zod";
@@ -278,6 +287,22 @@ var FrameHeader = z4.strictObject({
278
287
  sid: u32,
279
288
  seq: u32
280
289
  });
290
+ var RelayHopEnvelopeHeader = z4.strictObject({
291
+ version: z4.literal(FRAME_VERSION),
292
+ flags: z4.literal(0),
293
+ channel: z4.literal(255),
294
+ cid: u32,
295
+ sid: z4.literal(0),
296
+ seq: z4.literal(0)
297
+ });
298
+ var RelayHopConnectionEnd = z4.strictObject({
299
+ version: z4.literal(FRAME_VERSION),
300
+ flags: z4.literal(1),
301
+ channel: z4.literal(255),
302
+ cid: z4.int().min(1).max(4294967295),
303
+ sid: z4.literal(0),
304
+ seq: z4.literal(0)
305
+ });
281
306
  var DEFAULT_WINDOW_BYTES = 262144;
282
307
  var DEFAULT_MAX_FRAME_PAYLOAD_BYTES = 65536;
283
308
 
@@ -587,6 +612,22 @@ var RelayCallResponse = z9.discriminatedUnion("ok", [
587
612
  z9.strictObject({ ok: z9.literal(true), result: z9.unknown() }),
588
613
  z9.strictObject({ ok: z9.literal(false), error: ProtocolErrorBody })
589
614
  ]);
615
+ var RELAY_CALL_HELLO_ID = "hello";
616
+ var RELAY_CALL_REQUEST_ID = "call";
617
+ var RelayCallHello = z9.strictObject({
618
+ type: z9.literal("hello"),
619
+ id: z9.literal(RELAY_CALL_HELLO_ID),
620
+ protocolVersion: ProtocolVersion,
621
+ clientType: z9.literal("server"),
622
+ clientVersion: z9.string().min(1).max(32)
623
+ });
624
+ var RelayCallEnvelope = z9.strictObject({
625
+ id: z9.literal(RELAY_CALL_REQUEST_ID),
626
+ type: z9.literal("request"),
627
+ cap: z9.string().min(1).max(32),
628
+ op: z9.string().min(1).max(64),
629
+ params: z9.unknown()
630
+ });
590
631
 
591
632
  // src/schema/registry.ts
592
633
  var FS_ERR = ["NOT_FOUND", "PERMISSION_DENIED_OS", "INVALID_PARAMS", "RESOURCE_BUSY", "INTERNAL"];
@@ -691,7 +732,9 @@ function createDesktopClient(options) {
691
732
  let helloId = null;
692
733
  let reconnectTimer = null;
693
734
  let heartbeatTimer = null;
735
+ let probeTimer = null;
694
736
  let lastHeardAt = 0;
737
+ let heardCount = 0;
695
738
  let closedByUser = false;
696
739
  const newId = (prefix) => `${prefix}${(++nextId).toString(36)}`;
697
740
  function setState(patch) {
@@ -746,6 +789,7 @@ function createDesktopClient(options) {
746
789
  ws.onmessage = (event) => {
747
790
  if (generation !== socketGeneration) return;
748
791
  lastHeardAt = timers.now();
792
+ heardCount++;
749
793
  onMessage(event.data);
750
794
  };
751
795
  ws.onerror = () => {
@@ -787,6 +831,10 @@ function createDesktopClient(options) {
787
831
  timers.clearTimeout(heartbeatTimer);
788
832
  heartbeatTimer = null;
789
833
  }
834
+ if (probeTimer !== null) {
835
+ timers.clearTimeout(probeTimer);
836
+ probeTimer = null;
837
+ }
790
838
  const terminal = code === null ? void 0 : TERMINAL_CLOSE.get(code);
791
839
  const lost = error ?? (terminal ? new DesktopProtocolError(terminal[0], terminal[1]) : new DesktopProtocolError("DEVICE_OFFLINE", "Connection lost", { retryable: true }));
792
840
  for (const [id, p] of pending) {
@@ -1163,15 +1211,16 @@ function createDesktopClient(options) {
1163
1211
  });
1164
1212
  result.catch(() => void 0);
1165
1213
  done.catch(() => void 0);
1214
+ const attach = op === "session.attach" ? params : null;
1166
1215
  const s = {
1167
1216
  op,
1168
1217
  handlers,
1169
1218
  options: opts,
1170
1219
  id: newId("s"),
1171
1220
  sid: null,
1172
- resourceId: null,
1173
- lastSeq: 0,
1174
- expectSeq: 1,
1221
+ resourceId: attach ? attach.resource_id : null,
1222
+ lastSeq: attach?.since_seq ?? 0,
1223
+ expectSeq: attach ? attach.since_seq === void 0 ? null : attach.since_seq + 1 : 1,
1175
1224
  windowBytes: opts.windowBytes ?? DEFAULT_WINDOW_BYTES,
1176
1225
  consumedUncredited: 0,
1177
1226
  sendWindow: DEFAULT_WINDOW_BYTES,
@@ -1260,6 +1309,34 @@ function createDesktopClient(options) {
1260
1309
  function reauth(token) {
1261
1310
  sendJson({ type: "relay", op: "reauth", token });
1262
1311
  }
1312
+ function wake(opts = {}) {
1313
+ if (closedByUser || state.status === "closed") return;
1314
+ if (state.status === "idle" || state.status === "reconnecting") {
1315
+ connect();
1316
+ return;
1317
+ }
1318
+ if (state.status === "connecting" || !socket || probeTimer !== null) return;
1319
+ const generation = socketGeneration;
1320
+ const heardBefore = heardCount;
1321
+ try {
1322
+ socket.send(PING_TEXT);
1323
+ } catch {
1324
+ }
1325
+ probeTimer = timers.setTimeout(() => {
1326
+ probeTimer = null;
1327
+ if (generation !== socketGeneration || !socket) return;
1328
+ if (heardCount > heardBefore) return;
1329
+ const silentMs = timers.now() - lastHeardAt;
1330
+ diagnose({ kind: "heartbeat_timeout", silentMs });
1331
+ const dead = socket;
1332
+ onSocketGone(generation, null, new DesktopProtocolError("TIMEOUT", "Connection went silent", { retryable: true }));
1333
+ try {
1334
+ dead.close(CloseCode.NORMAL, "wake probe timeout");
1335
+ } catch {
1336
+ }
1337
+ if (state.status === "reconnecting") connect();
1338
+ }, opts.probeMs ?? 4e3);
1339
+ }
1263
1340
  return {
1264
1341
  connect,
1265
1342
  close,
@@ -1274,12 +1351,35 @@ function createDesktopClient(options) {
1274
1351
  request,
1275
1352
  stream,
1276
1353
  on,
1277
- reauth
1354
+ reauth,
1355
+ wake
1356
+ };
1357
+ }
1358
+
1359
+ // src/client/wake.ts
1360
+ function bindDesktopWake(client, options = {}) {
1361
+ const targets = options.targets ?? (typeof window !== "undefined" && typeof document !== "undefined" ? { window, document } : null);
1362
+ if (!targets) return () => void 0;
1363
+ const wakeOptions = options.probeMs === void 0 ? {} : { probeMs: options.probeMs };
1364
+ const onVisibility = () => {
1365
+ if (targets.document.visibilityState === "visible") client.wake(wakeOptions);
1366
+ };
1367
+ const onWake = () => client.wake(wakeOptions);
1368
+ targets.document.addEventListener("visibilitychange", onVisibility);
1369
+ targets.window.addEventListener("pageshow", onWake);
1370
+ targets.window.addEventListener("online", onWake);
1371
+ targets.window.addEventListener("focus", onWake);
1372
+ return () => {
1373
+ targets.document.removeEventListener("visibilitychange", onVisibility);
1374
+ targets.window.removeEventListener("pageshow", onWake);
1375
+ targets.window.removeEventListener("online", onWake);
1376
+ targets.window.removeEventListener("focus", onWake);
1278
1377
  };
1279
1378
  }
1280
1379
  export {
1281
1380
  DesktopProtocolError,
1282
1381
  RECONNECT_ALARM_ATTEMPTS,
1382
+ bindDesktopWake,
1283
1383
  createDesktopClient,
1284
1384
  isDesktopProtocolError
1285
1385
  };