@hypen-space/core 0.4.26 → 0.4.27

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.
@@ -2,7 +2,7 @@
2
2
  * Remote UI Protocol Types
3
3
  */
4
4
  import type { Patch } from "../types.js";
5
- export type RemoteMessage = InitialTreeMessage | PatchMessage | StateUpdateMessage | DispatchActionMessage | HelloMessage | SessionAckMessage | SessionExpiredMessage;
5
+ export type RemoteMessage = InitialTreeMessage | PatchMessage | StateUpdateMessage | UpdateStateMessage | SubscribeStateMessage | DispatchActionMessage | HelloMessage | SessionAckMessage | SessionExpiredMessage;
6
6
  export interface InitialTreeMessage {
7
7
  type: "initialTree";
8
8
  module: string;
@@ -28,6 +28,23 @@ export interface DispatchActionMessage {
28
28
  action: string;
29
29
  payload?: any;
30
30
  }
31
+ /**
32
+ * Client → Server: Override the module's state (e.g. for time-travel)
33
+ * Server will update the module instance and re-render, sending back
34
+ * patch + stateUpdate messages.
35
+ */
36
+ export interface UpdateStateMessage {
37
+ type: "updateState";
38
+ module: string;
39
+ state: any;
40
+ }
41
+ /**
42
+ * Client → Server: Opt-in to receiving stateUpdate messages after each render.
43
+ * Only subscribed clients receive state snapshots (e.g. Studio for time-travel).
44
+ */
45
+ export interface SubscribeStateMessage {
46
+ type: "subscribeState";
47
+ }
31
48
  /**
32
49
  * Client → Server: First message after WebSocket opens
33
50
  * Used to establish or resume a session
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hypen-space/core",
3
- "version": "0.4.26",
3
+ "version": "0.4.27",
4
4
  "description": "Hypen core engine - Platform-agnostic reactive UI runtime",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
@@ -85,6 +85,7 @@
85
85
  "./remote/client": {
86
86
  "types": "./dist/remote/client.d.ts",
87
87
  "bun": "./src/remote/client.ts",
88
+ "browser": "./src/remote/client.ts",
88
89
  "import": "./dist/remote/client.js",
89
90
  "default": "./dist/remote/client.js"
90
91
  },
@@ -32,6 +32,8 @@ import type {
32
32
  InitialTreeMessage,
33
33
  PatchMessage,
34
34
  DispatchActionMessage,
35
+ UpdateStateMessage,
36
+ SubscribeStateMessage,
35
37
  HelloMessage,
36
38
  SessionAckMessage,
37
39
  SessionExpiredMessage,
@@ -278,6 +280,39 @@ export class RemoteEngine implements Disposable {
278
280
  this.ws.send(JSON.stringify(message));
279
281
  }
280
282
 
283
+ /**
284
+ * Subscribe to state updates after each render.
285
+ * Only subscribed clients receive stateUpdate messages (avoids overhead for native clients).
286
+ */
287
+ subscribeState(): void {
288
+ if (this.state !== "connected" || !this.ws) {
289
+ log.warn("Cannot subscribe to state: not connected");
290
+ return;
291
+ }
292
+
293
+ const message: SubscribeStateMessage = { type: "subscribeState" };
294
+ this.ws.send(JSON.stringify(message));
295
+ }
296
+
297
+ /**
298
+ * Override the remote module's state (e.g. for time-travel).
299
+ * The server will update state, re-render, and send back patches.
300
+ */
301
+ updateState(state: unknown): void {
302
+ if (this.state !== "connected" || !this.ws) {
303
+ log.warn("Cannot update state: not connected");
304
+ return;
305
+ }
306
+
307
+ const message: UpdateStateMessage = {
308
+ type: "updateState",
309
+ module: this.moduleName,
310
+ state,
311
+ };
312
+
313
+ this.ws.send(JSON.stringify(message));
314
+ }
315
+
281
316
  /**
282
317
  * Register callback for patches
283
318
  */
@@ -37,6 +37,8 @@ import type {
37
37
  RemoteServerConfig,
38
38
  InitialTreeMessage,
39
39
  PatchMessage,
40
+ StateUpdateMessage,
41
+ UpdateStateMessage,
40
42
  DispatchActionMessage,
41
43
  HelloMessage,
42
44
  SessionAckMessage,
@@ -68,6 +70,8 @@ interface ClientData {
68
70
  helloReceived: boolean;
69
71
  /** Timeout for legacy clients that don't send hello */
70
72
  helloTimeout?: ReturnType<typeof setTimeout>;
73
+ /** Whether this client subscribes to state updates after each render */
74
+ stateSubscribed: boolean;
71
75
  }
72
76
 
73
77
  /**
@@ -341,6 +345,7 @@ export class RemoteServer {
341
345
  connectedAt,
342
346
  sessionId: "",
343
347
  helloReceived: false,
348
+ stateSubscribed: false,
344
349
  };
345
350
 
346
351
  this.clients.set(ws, clientData);
@@ -497,14 +502,25 @@ export class RemoteServer {
497
502
 
498
503
  data.revision++;
499
504
 
500
- const message: PatchMessage = {
505
+ const patchMessage: PatchMessage = {
501
506
  type: "patch",
502
507
  module: this._moduleName,
503
508
  patches,
504
509
  revision: data.revision,
505
510
  };
506
511
 
507
- ws.send(JSON.stringify(message));
512
+ ws.send(JSON.stringify(patchMessage));
513
+
514
+ // Only send state snapshot to clients that opted in (e.g. Studio)
515
+ if (data.stateSubscribed) {
516
+ const stateMessage: StateUpdateMessage = {
517
+ type: "stateUpdate",
518
+ module: this._moduleName,
519
+ state: data.moduleInstance.getState(),
520
+ revision: data.revision,
521
+ };
522
+ ws.send(JSON.stringify(stateMessage));
523
+ }
508
524
 
509
525
  // If allow-multiple, broadcast to other connections on same session
510
526
  if (
@@ -515,7 +531,18 @@ export class RemoteServer {
515
531
  if (connections) {
516
532
  for (const conn of connections) {
517
533
  if (conn !== ws) {
518
- (conn as ServerWebSocket<unknown>).send(JSON.stringify(message));
534
+ const otherWs = conn as ServerWebSocket<unknown>;
535
+ const otherData = this.clients.get(otherWs);
536
+ otherWs.send(JSON.stringify(patchMessage));
537
+ if (otherData?.stateSubscribed) {
538
+ const stateMessage: StateUpdateMessage = {
539
+ type: "stateUpdate",
540
+ module: this._moduleName,
541
+ state: data.moduleInstance.getState(),
542
+ revision: data.revision,
543
+ };
544
+ otherWs.send(JSON.stringify(stateMessage));
545
+ }
519
546
  }
520
547
  }
521
548
  }
@@ -637,6 +664,26 @@ export class RemoteServer {
637
664
  break;
638
665
  }
639
666
 
667
+ case "updateState": {
668
+ const stateMsg = msg as UpdateStateMessage;
669
+ clientData.moduleInstance.updateState(stateMsg.state);
670
+
671
+ // Sync mode: update state on all other clients' engines
672
+ if (this._syncActions) {
673
+ for (const [otherWs, otherClient] of this.clients) {
674
+ if (otherWs === ws || !otherClient.helloReceived) continue;
675
+ otherClient.moduleInstance.updateState(stateMsg.state);
676
+ }
677
+ }
678
+ break;
679
+ }
680
+
681
+ case "subscribeState": {
682
+ clientData.stateSubscribed = true;
683
+ log.info(`Client ${clientData.id} subscribed to state updates`);
684
+ break;
685
+ }
686
+
640
687
  default:
641
688
  // Unknown message type
642
689
  break;
@@ -8,6 +8,8 @@ export type RemoteMessage =
8
8
  | InitialTreeMessage
9
9
  | PatchMessage
10
10
  | StateUpdateMessage
11
+ | UpdateStateMessage
12
+ | SubscribeStateMessage
11
13
  | DispatchActionMessage
12
14
  | HelloMessage
13
15
  | SessionAckMessage
@@ -42,6 +44,25 @@ export interface DispatchActionMessage {
42
44
  payload?: any;
43
45
  }
44
46
 
47
+ /**
48
+ * Client → Server: Override the module's state (e.g. for time-travel)
49
+ * Server will update the module instance and re-render, sending back
50
+ * patch + stateUpdate messages.
51
+ */
52
+ export interface UpdateStateMessage {
53
+ type: "updateState";
54
+ module: string;
55
+ state: any;
56
+ }
57
+
58
+ /**
59
+ * Client → Server: Opt-in to receiving stateUpdate messages after each render.
60
+ * Only subscribed clients receive state snapshots (e.g. Studio for time-travel).
61
+ */
62
+ export interface SubscribeStateMessage {
63
+ type: "subscribeState";
64
+ }
65
+
45
66
  /**
46
67
  * Client → Server: First message after WebSocket opens
47
68
  * Used to establish or resume a session
Binary file
@@ -5,7 +5,7 @@
5
5
  "Hypen Contributors"
6
6
  ],
7
7
  "description": "A Rust implementation of the Hypen engine",
8
- "version": "0.4.26",
8
+ "version": "0.4.27",
9
9
  "license": "MIT",
10
10
  "repository": {
11
11
  "type": "git",
Binary file
@@ -4,7 +4,7 @@
4
4
  "Hypen Contributors"
5
5
  ],
6
6
  "description": "A Rust implementation of the Hypen engine",
7
- "version": "0.4.26",
7
+ "version": "0.4.27",
8
8
  "license": "MIT",
9
9
  "repository": {
10
10
  "type": "git",