@jxsuite/server 2.1.0 → 2.2.1

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 (2) hide show
  1. package/package.json +12 -12
  2. package/src/project-server.ts +40 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/server",
3
- "version": "2.1.0",
3
+ "version": "2.2.1",
4
4
  "description": "Jx development server with live reload, server-side proxy, and studio integration",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -38,21 +38,21 @@
38
38
  "upgrade": "bunx npm-check-updates -u && bun install"
39
39
  },
40
40
  "dependencies": {
41
- "@jxsuite/collab": "^0.7.0",
42
- "@jxsuite/compiler": "^2.0.0",
43
- "@jxsuite/create": "^1.3.0",
44
- "@jxsuite/import": "^0.39.0",
45
- "@jxsuite/protocol": "^1.1.0",
46
- "@jxsuite/runtime": "^2.0.0",
47
- "@jxsuite/schema": "^1.6.0",
48
- "@jxsuite/starters": "^1.3.0",
41
+ "@jxsuite/collab": "^0.8.1",
42
+ "@jxsuite/compiler": "^2.0.2",
43
+ "@jxsuite/create": "^1.3.2",
44
+ "@jxsuite/import": "^0.39.2",
45
+ "@jxsuite/protocol": "^1.1.2",
46
+ "@jxsuite/runtime": "^2.0.2",
47
+ "@jxsuite/schema": "^1.8.0",
48
+ "@jxsuite/starters": "^1.5.0",
49
49
  "chokidar": "^5.0.0",
50
50
  "kysely": "^0.29.5",
51
51
  "zod": "^4.4.3"
52
52
  },
53
53
  "devDependencies": {
54
- "@jxsuite/auth": "^0.5.0",
55
- "@jxsuite/connector": "^0.5.0",
56
- "@jxsuite/parser": "^1.5.0"
54
+ "@jxsuite/auth": "^0.5.2",
55
+ "@jxsuite/connector": "^0.5.2",
56
+ "@jxsuite/parser": "^1.5.2"
57
57
  }
58
58
  }
@@ -26,6 +26,7 @@
26
26
  */
27
27
 
28
28
  import { resolve, sep } from "node:path";
29
+ import type { ServerWebSocket } from "bun";
29
30
  import { handleJxMounts } from "./jx-mounts.ts";
30
31
  import { handleResolve, handleServerFunction, projectAssetMounts } from "./resolve.ts";
31
32
  import { handleAiApi } from "./ai-api.ts";
@@ -77,6 +78,18 @@ export interface ProjectServerHandle {
77
78
  wsUrl: string;
78
79
  rpcToken: string;
79
80
  canvasUrl: string;
81
+ /**
82
+ * Push an UNSOLICITED message to the connected shells — the server-to-client half of the RPC.
83
+ *
84
+ * Every other frame on this socket answers an `id` the client chose; these carry a `method` and
85
+ * no `id`, which is how the client's dispatcher tells the two apart. It exists because some
86
+ * things the shell must know are not answers to anything it asked: a file changed on disk, or
87
+ * another window wants this one raised.
88
+ *
89
+ * @param winId Deliver only to sockets stashed with this window id; omit for every socket.
90
+ * @returns How many sockets the message reached — 0 means nothing is listening yet.
91
+ */
92
+ push: (method: string, params?: unknown, winId?: string | null) => number;
80
93
  /**
81
94
  * The RFC 8252 loopback authorization host. The desktop launcher drives it: `begin()` for the URL
82
95
  * to open in the user's browser, then `await pending.code` and {@link exchangeCode}.
@@ -106,6 +119,11 @@ export function createProjectServer(options: CreateProjectServerOptions): Projec
106
119
  /* Outstanding OAuth authorizations (RFC 8252). Per server, so a stopped server abandons them. */
107
120
  const authorizer: LoopbackAuthorizer = createLoopbackAuthorizer();
108
121
 
122
+ /* Live shell sockets, so the server can speak first (see ProjectServerHandle.push). Membership is
123
+ the socket's whole lifetime — added on open, dropped on close — so a push after a window is
124
+ gone reaches nobody rather than throwing. */
125
+ const sockets = new Set<ServerWebSocket<{ winId: string | null }>>();
126
+
109
127
  const server = Bun.serve<{ winId: string | null }>({
110
128
  hostname,
111
129
  port,
@@ -312,6 +330,12 @@ export function createProjectServer(options: CreateProjectServerOptions): Projec
312
330
  },
313
331
 
314
332
  websocket: {
333
+ open(ws) {
334
+ sockets.add(ws);
335
+ },
336
+ close(ws) {
337
+ sockets.delete(ws);
338
+ },
315
339
  async message(ws, raw) {
316
340
  let msg: { id: number; method: string; params?: unknown };
317
341
  try {
@@ -362,6 +386,22 @@ export function createProjectServer(options: CreateProjectServerOptions): Projec
362
386
  rpcToken,
363
387
  canvasUrl,
364
388
  authorizer,
389
+ push: (method, params, winId) => {
390
+ const frame = JSON.stringify(params === undefined ? { method } : { method, params });
391
+ let delivered = 0;
392
+ for (const ws of sockets) {
393
+ if (winId !== undefined && winId !== null && ws.data.winId !== winId) {
394
+ continue;
395
+ }
396
+ try {
397
+ ws.send(frame);
398
+ delivered += 1;
399
+ } catch {
400
+ // A socket that closed between the iteration and the send is simply not a recipient.
401
+ }
402
+ }
403
+ return delivered;
404
+ },
365
405
  stop: () => {
366
406
  // Abandon outstanding sign-ins first: their callbacks can no longer arrive.
367
407
  authorizer.stop();