@jxsuite/server 0.37.1 → 1.1.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/package.json +20 -10
- package/src/collab.ts +29 -33
- package/src/data-api.ts +874 -0
- package/src/dev-vars.ts +61 -0
- package/src/dev.ts +141 -0
- package/src/jx-mounts.ts +166 -0
- package/src/project-server.ts +10 -0
- package/src/resolve.ts +20 -12
- package/src/server.ts +10 -0
- package/src/studio-api.ts +53 -13
- package/src/watch.ts +17 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jxsuite/server",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.1.0",
|
|
4
4
|
"description": "Jx development server with live reload, server-side proxy, and studio integration",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -14,13 +14,17 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"exports": {
|
|
16
16
|
".": "./src/server.ts",
|
|
17
|
+
"./dev": "./src/dev.ts",
|
|
17
18
|
"./ai-api": "./src/ai-api.ts",
|
|
19
|
+
"./data": "./src/data-api.ts",
|
|
18
20
|
"./import-api": "./src/import-api.ts",
|
|
19
21
|
"./packages": "./src/packages.ts",
|
|
20
22
|
"./project-server": "./src/project-server.ts",
|
|
21
23
|
"./refactor": "./src/refactor/index.ts",
|
|
22
24
|
"./resolve": "./src/resolve.ts",
|
|
23
|
-
"./types": "./src/types.ts"
|
|
25
|
+
"./types": "./src/types.ts",
|
|
26
|
+
"./jx-mounts": "./src/jx-mounts.ts",
|
|
27
|
+
"./dev-vars": "./src/dev-vars.ts"
|
|
24
28
|
},
|
|
25
29
|
"publishConfig": {
|
|
26
30
|
"provenance": true
|
|
@@ -31,15 +35,21 @@
|
|
|
31
35
|
"test:coverage": "bun test --isolate --coverage"
|
|
32
36
|
},
|
|
33
37
|
"dependencies": {
|
|
34
|
-
"@jxsuite/collab": "^0.
|
|
35
|
-
"@jxsuite/compiler": "^
|
|
36
|
-
"@jxsuite/create": "^
|
|
37
|
-
"@jxsuite/import": "^0.
|
|
38
|
-
"@jxsuite/
|
|
39
|
-
"@jxsuite/runtime": "^
|
|
40
|
-
"@jxsuite/schema": "^
|
|
41
|
-
"@jxsuite/starters": "^
|
|
38
|
+
"@jxsuite/collab": "^0.4.0",
|
|
39
|
+
"@jxsuite/compiler": "^1.1.0",
|
|
40
|
+
"@jxsuite/create": "^1.1.0",
|
|
41
|
+
"@jxsuite/import": "^0.37.0",
|
|
42
|
+
"@jxsuite/protocol": "^0.5.0",
|
|
43
|
+
"@jxsuite/runtime": "^1.1.0",
|
|
44
|
+
"@jxsuite/schema": "^1.1.0",
|
|
45
|
+
"@jxsuite/starters": "^1.1.0",
|
|
42
46
|
"chokidar": "^5.0.0",
|
|
47
|
+
"kysely": "^0.29.3",
|
|
43
48
|
"zod": "^4.4.3"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@jxsuite/auth": "^0.3.0",
|
|
52
|
+
"@jxsuite/connector": "^0.3.0",
|
|
53
|
+
"@jxsuite/parser": "^1.1.0"
|
|
44
54
|
}
|
|
45
55
|
}
|
package/src/collab.ts
CHANGED
|
@@ -2,11 +2,15 @@
|
|
|
2
2
|
* Collab.ts — the dev server's realtime co-editing endpoint (`/__studio/collab`): the reference
|
|
3
3
|
* implementation of the Jx collab wire contract, built on `@jxsuite/collab/room`.
|
|
4
4
|
*
|
|
5
|
-
* Rooms are keyed by server-root-relative path; a room's Y.Text source seeds from the file on disk
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
5
|
+
* Rooms are keyed by server-root-relative path; a room's Y.Text source seeds from the file on disk.
|
|
6
|
+
* Persistence is EXPLICIT: the shared source is written back to disk only on an explicit flush
|
|
7
|
+
* (Cmd+S / Save) and, as a last-ditch safety, on graceful server shutdown. Unsaved collaborative
|
|
8
|
+
* edits therefore live only in the room's in-memory Y.Doc until saved — non-collab consumers
|
|
9
|
+
* (preview, live reload, plain editors) intentionally keep working off the last-saved file until
|
|
10
|
+
* then. The host owns the room-level dirty signal broadcast to every peer; the embedder just tells
|
|
11
|
+
* it a persist landed via `markPersisted`. Genuinely external file changes (not our own write-back
|
|
12
|
+
* echo) bump the doc's epoch and reset subscribers, per the contract. No persistence beyond the
|
|
13
|
+
* files themselves: this is deliberately a reference implementation.
|
|
10
14
|
*/
|
|
11
15
|
|
|
12
16
|
import { resolve } from "node:path";
|
|
@@ -17,7 +21,6 @@ import { assertAccessible } from "./studio-api";
|
|
|
17
21
|
import type { ServerWebSocket, WebSocketHandler } from "bun";
|
|
18
22
|
import type { HostConnection } from "@jxsuite/collab/room";
|
|
19
23
|
|
|
20
|
-
const WRITE_BACK_DEBOUNCE_MS = 800;
|
|
21
24
|
const EMPTY_ROOM_GRACE_MS = 30_000;
|
|
22
25
|
|
|
23
26
|
const BINARY_EXTENSIONS = new Set([
|
|
@@ -79,8 +82,9 @@ export function createCollabRegistry(opts: {
|
|
|
79
82
|
let connectionCounter = 0;
|
|
80
83
|
let stopped = false;
|
|
81
84
|
|
|
82
|
-
const writeTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
83
85
|
const destroyTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
86
|
+
/** Paths with unsaved edits (source changed since the last persist) — the shutdown worklist. */
|
|
87
|
+
const pendingPersist = new Set<string>();
|
|
84
88
|
/** Content we last wrote (or are about to write) per path — the watcher-echo signature. */
|
|
85
89
|
const lastWritten = new Map<string, string>();
|
|
86
90
|
|
|
@@ -103,13 +107,19 @@ export function createCollabRegistry(opts: {
|
|
|
103
107
|
return;
|
|
104
108
|
}
|
|
105
109
|
if (lastWritten.get(path) === source) {
|
|
110
|
+
pendingPersist.delete(path);
|
|
106
111
|
return;
|
|
107
112
|
}
|
|
113
|
+
// Optimistic: closes the watcher-echo race (our own write must not read back as external).
|
|
108
114
|
lastWritten.set(path, source);
|
|
109
115
|
try {
|
|
110
116
|
await writeFile(absPathFor(path), source, "utf8");
|
|
117
|
+
pendingPersist.delete(path);
|
|
118
|
+
// Tell the host the room is durable now: it resets the dirty baseline and broadcasts clean.
|
|
119
|
+
host.markPersisted(path);
|
|
111
120
|
} catch {
|
|
112
|
-
//
|
|
121
|
+
// Write failed — clear the optimistic signature so the next explicit save retries.
|
|
122
|
+
lastWritten.delete(path);
|
|
113
123
|
}
|
|
114
124
|
};
|
|
115
125
|
|
|
@@ -126,34 +136,22 @@ export function createCollabRegistry(opts: {
|
|
|
126
136
|
if (existing) {
|
|
127
137
|
clearTimeout(existing);
|
|
128
138
|
}
|
|
139
|
+
// Warnings-only model: an empty room is NOT auto-persisted (the last collaborator was warned
|
|
140
|
+
// Before leaving). Its unsaved edits are dropped when the room is torn down after the grace.
|
|
129
141
|
destroyTimers.set(
|
|
130
142
|
path,
|
|
131
143
|
setTimeout(() => {
|
|
132
144
|
destroyTimers.delete(path);
|
|
133
|
-
|
|
145
|
+
pendingPersist.delete(path);
|
|
146
|
+
host.destroyRoomIfEmpty(path);
|
|
134
147
|
}, EMPTY_ROOM_GRACE_MS),
|
|
135
148
|
);
|
|
136
149
|
},
|
|
137
|
-
onFlush: (path) =>
|
|
138
|
-
const timer = writeTimers.get(path);
|
|
139
|
-
if (timer) {
|
|
140
|
-
clearTimeout(timer);
|
|
141
|
-
writeTimers.delete(path);
|
|
142
|
-
}
|
|
143
|
-
return persist(path);
|
|
144
|
-
},
|
|
150
|
+
onFlush: (path) => persist(path),
|
|
145
151
|
onSourceChange: (path) => {
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
}
|
|
150
|
-
writeTimers.set(
|
|
151
|
-
path,
|
|
152
|
-
setTimeout(() => {
|
|
153
|
-
writeTimers.delete(path);
|
|
154
|
-
void persist(path);
|
|
155
|
-
}, WRITE_BACK_DEBOUNCE_MS),
|
|
156
|
-
);
|
|
152
|
+
// No write-back: edits reach disk only on explicit flush (or graceful shutdown). The host
|
|
153
|
+
// Owns the dirty broadcast; we just note the path for the shutdown worklist.
|
|
154
|
+
pendingPersist.add(path);
|
|
157
155
|
},
|
|
158
156
|
rejectPath: (path) => {
|
|
159
157
|
if (isBinaryPath(path)) {
|
|
@@ -205,17 +203,15 @@ export function createCollabRegistry(opts: {
|
|
|
205
203
|
return;
|
|
206
204
|
}
|
|
207
205
|
stopped = true;
|
|
208
|
-
for (const timer of writeTimers.values()) {
|
|
209
|
-
clearTimeout(timer);
|
|
210
|
-
}
|
|
211
206
|
for (const timer of destroyTimers.values()) {
|
|
212
207
|
clearTimeout(timer);
|
|
213
208
|
}
|
|
209
|
+
// Last-ditch safety on graceful shutdown: flush every path with unsaved edits.
|
|
214
210
|
const flushes: Promise<void>[] = [];
|
|
215
|
-
for (const path of
|
|
211
|
+
for (const path of pendingPersist) {
|
|
216
212
|
flushes.push(persist(path));
|
|
217
213
|
}
|
|
218
|
-
|
|
214
|
+
pendingPersist.clear();
|
|
219
215
|
destroyTimers.clear();
|
|
220
216
|
await Promise.all(flushes);
|
|
221
217
|
host.destroy();
|