@jxsuite/protocol 2.3.1 → 2.5.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/README.md CHANGED
@@ -17,7 +17,7 @@ A backend is free to change the transport and path prefix; the **sub-path, metho
17
17
 
18
18
  ## Core vs optional
19
19
 
20
- Optional routes back optional `StudioPlatform` members. Omitting one is not an error. Studio degrades exactly as each entry's `degradation` describes (the starter picker empties, the Projects catalogue hides, the Publish panel explains git-push publishing, and so on). `coreRouteNames()` / `optionalRouteNames()` split the table programmatically; a conformance test for a new backend can iterate `STUDIO_ROUTES` and assert every core route is served.
20
+ Optional routes back optional `StudioPlatform` members, or, like `filesRead`, back no member and let an adapter serve one it already has more cheaply (their `degradation` names the route they stand in for). Omitting one is not an error. Studio degrades exactly as each entry's `degradation` describes (the starter picker empties, the Projects catalogue hides, the Publish panel explains git-push publishing, and so on). `coreRouteNames()` / `optionalRouteNames()` split the table programmatically; a conformance test for a new backend can iterate `STUDIO_ROUTES` and assert every core route is served.
21
21
 
22
22
  Highlights an implementer should know:
23
23
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jxsuite/protocol",
3
- "version": "2.3.1",
3
+ "version": "2.5.0",
4
4
  "description": "The Studio Backend Protocol — wire types and the canonical route table every Jx Studio backend implements",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -27,6 +27,6 @@
27
27
  "test:coverage": "bun test --isolate --coverage"
28
28
  },
29
29
  "dependencies": {
30
- "@jxsuite/schema": "^2.2.0"
30
+ "@jxsuite/schema": "^2.3.0"
31
31
  }
32
32
  }
package/src/index.ts CHANGED
@@ -1,7 +1,6 @@
1
1
  /**
2
- * The Studio Backend Protocol package: wire types plus the canonical route
3
- * table every Jx Studio backend implements. See README.md for the implementer
4
- * guide.
2
+ * The Studio Backend Protocol package: wire types plus the canonical route table every Jx Studio
3
+ * backend implements. See README.md for the implementer guide.
5
4
  *
6
5
  * @license MIT
7
6
  */
@@ -10,3 +9,5 @@ export * from "./problem";
10
9
  export * from "./problems";
11
10
  export * from "./routes";
12
11
  export type * from "./types";
12
+ // The one runtime value types.ts carries: the batch-read limit a backend and a client both size to.
13
+ export { READ_FILES_MAX_PATHS } from "./types";
package/src/routes.ts CHANGED
@@ -1,10 +1,10 @@
1
1
  /**
2
- * Studio Backend Protocol — the canonical route table. The dev server
3
- * (@jxsuite/server) is the reference implementation; every other backend
4
- * (desktop RPC bridge, cloud platforms) serves the same shapes, either at
5
- * these literal paths or through an equivalent transport. Optional routes
6
- * back optional StudioPlatform members — Studio degrades without them, as
7
- * described by each entry's `degradation`.
2
+ * Studio Backend Protocol — the canonical route table. The dev server (@jxsuite/server) is the
3
+ * reference implementation; every other backend (desktop RPC bridge, cloud platforms) serves the
4
+ * same shapes, either at these literal paths or through an equivalent transport. Optional routes
5
+ * mostly back optional StudioPlatform members — Studio degrades without them, as described by each
6
+ * entry's `degradation`. A few back no member at all: an adapter uses them to serve members it
7
+ * already has more cheaply (`filesRead`), and their `degradation` is the route they stand in for.
8
8
  *
9
9
  * @license MIT
10
10
  */
@@ -28,7 +28,10 @@ export type StudioRouteMethod = "GET" | "POST" | "PUT" | "DELETE";
28
28
  export interface StudioRoute {
29
29
  path: string;
30
30
  method: StudioRouteMethod;
31
- /** True when a backend may omit the route (its PAL member is optional). */
31
+ /**
32
+ * True when a backend may omit the route: its PAL member is optional, or it has no member and an
33
+ * adapter falls back to another route.
34
+ */
32
35
  optional: boolean;
33
36
  /** One-line contract summary. */
34
37
  summary: string;
@@ -83,7 +86,7 @@ export const STUDIO_ROUTES = {
83
86
  "GET",
84
87
  "/__studio/locate-directory",
85
88
  `Resolve the absolute path of a showDirectoryPicker() folder by the id in its ${LOCATION_ID_FILE}`,
86
- "The New Project Location field loses its Browse… button and is typed by hand.",
89
+ "Browse… reports that the folder could not be placed; the Location is typed by hand.",
87
90
  ),
88
91
  createProject: route(
89
92
  "POST",
@@ -111,6 +114,31 @@ export const STUDIO_ROUTES = {
111
114
  "files project-wide",
112
115
  ),
113
116
  fileRead: route("GET", "/__studio/file", "Read a file's text content"),
117
+ /**
118
+ * Many reads in ONE round trip, for a backend whose every request is a network hop.
119
+ *
120
+ * The first optional route with no `StudioPlatform` member behind it: the PAL keeps `readFile`,
121
+ * and an adapter for such a backend coalesces the reads issued in one tick into this call itself
122
+ * (specs/desktop.md §3.1, §10.1). Callers never see it, so a backend without it costs latency and
123
+ * nothing else — which is why the dev server, whose reads are loopback, does not serve it.
124
+ *
125
+ * The answer is partial by design, never a 400: past `READ_FILES_MAX_PATHS` paths or the
126
+ * backend's byte budget, the rest come back in `omitted` for the client to ask again, and the
127
+ * first path is always answered so that asking again makes progress. Each answered path carries
128
+ * its own outcome, because one missing file is not a failed batch. It is a READ: a read-only
129
+ * session may call it, and whatever a single `fileRead` does on the backend's side (a cloud
130
+ * session fetches and stores the file it serves) this does per path too.
131
+ */
132
+ filesRead: route(
133
+ "POST",
134
+ "/__studio/files/read",
135
+ "Read many files' text content {paths} → ReadFilesResult {files: [path, {content} | " +
136
+ "{error: {status, code?, message}}][], omitted}. At most READ_FILES_MAX_PATHS (200) paths " +
137
+ "and a backend byte budget per call: the excess is listed in `omitted` (never a 400) and " +
138
+ "the first path is always answered. Per-path errors: 404 missing/deleted, 415 binary, 413 " +
139
+ "too large, 502 upstream. Allowed for read-only sessions; keeps fileRead's server-side effects",
140
+ "Adapters fall back to one fileRead per path.",
141
+ ),
114
142
  fileWrite: route("PUT", "/__studio/file", "Write a file's text content"),
115
143
  fileDelete: route("DELETE", "/__studio/file", "Delete a file"),
116
144
  fileUpload: route(
@@ -119,6 +147,31 @@ export const STUDIO_ROUTES = {
119
147
  "Upload binary content to a path → UploadResult {path, size?}. `path` is the answer, not an " +
120
148
  "echo: a store that de-duplicates, suffixes, or normalizes names reports what it really wrote",
121
149
  ),
150
+ /**
151
+ * A project file's raw BYTES, as an answer rather than as a URL.
152
+ *
153
+ * `fileRead` cannot serve this and never could: it answers `readFile(abs, "utf8")`, and Bun
154
+ * replaces every byte sequence that is not valid UTF-8 with U+FFFD — so a JPEG round-trips
155
+ * through it as mojibake, silently and losslessly-looking. A caller that wants to DECODE a
156
+ * project image (Studio's image editor) needs the bytes intact.
157
+ *
158
+ * Distinct from {@link documentRaw}, which serves files at a URL a `<img src>` or a `fetch` in
159
+ * the canvas realm can name. That is the right shape for rendering and the wrong one for reading:
160
+ * on the desktop the canvas origin is a cross-origin loopback, and `server.md` §4.2 bans CORS
161
+ * outright, so nothing in the shell can read those bytes back. This route is addressed by the
162
+ * PAL, not by a document, which is what lets every backend answer it the same way.
163
+ *
164
+ * `Cache-Control: no-store`, deliberately unlike `fileRead`'s `max-age=5`: the caller is an
165
+ * editor about to overwrite the file, and five seconds of staleness is an edit applied to the
166
+ * wrong bytes.
167
+ */
168
+ fileBytes: route(
169
+ "GET",
170
+ "/__studio/file/bytes",
171
+ "A project file's raw bytes, undecoded — the read side of `fileUpload`, for a caller that must " +
172
+ "not go through UTF-8",
173
+ "Studio can only read a project file as text, so any surface that must decode one stays hidden.",
174
+ ),
122
175
  /**
123
176
  * A project file as its own BYTES, at its own URL.
124
177
  *
@@ -450,7 +503,10 @@ export function coreRouteNames(): StudioRouteName[] {
450
503
  );
451
504
  }
452
505
 
453
- /** Names of the optional routes (each backs an optional StudioPlatform member). */
506
+ /**
507
+ * Names of the optional routes (each backs an optional StudioPlatform member, or is an adapter's
508
+ * optimisation with a fallback).
509
+ */
454
510
  export function optionalRouteNames(): StudioRouteName[] {
455
511
  return (Object.keys(STUDIO_ROUTES) as StudioRouteName[]).filter(
456
512
  (name) => STUDIO_ROUTES[name].optional,
package/src/types.ts CHANGED
@@ -1,8 +1,7 @@
1
1
  /**
2
- * Studio Backend Protocol — wire types. Every shape a Studio backend serves
3
- * (or a `StudioPlatform` adapter consumes) lives here so client adapters and
4
- * server implementations (the dev server, the desktop RPC bridge, cloud
5
- * platforms) share one contract. Environment-agnostic: no DOM, no node —
2
+ * Studio Backend Protocol — wire types. Every shape a Studio backend serves (or a `StudioPlatform`
3
+ * adapter consumes) lives here so client adapters and server implementations (the dev server, the
4
+ * desktop RPC bridge, cloud platforms) share one contract. Environment-agnostic: no DOM, no node —
6
5
  * importable in browsers, Bun, and Cloudflare Workers alike.
7
6
  *
8
7
  * @license MIT
@@ -87,6 +86,45 @@ export interface UploadResult {
87
86
  size?: number;
88
87
  }
89
88
 
89
+ /**
90
+ * The most paths one `filesRead` request may name. A backend answers at most this many and lists
91
+ * the rest under {@link ReadFilesResult.omitted}, so a client that sends more is not refused — it is
92
+ * answered in part, and asks again.
93
+ *
94
+ * The one runtime value in this file: a client sizes its batches to it, so it has to exist at
95
+ * runtime rather than only in a summary string.
96
+ */
97
+ export const READ_FILES_MAX_PATHS = 200;
98
+
99
+ /** Body of `POST /__studio/files/read` (`filesRead`): the project-relative paths to read. */
100
+ export interface ReadFilesRequest {
101
+ paths: string[];
102
+ }
103
+
104
+ /**
105
+ * One path's answer inside a {@link ReadFilesResult}: its text, or why it has none.
106
+ *
107
+ * Per path rather than per request, because one unreadable file in a batch of two hundred is not a
108
+ * failed batch. `status` borrows the HTTP code the single-file read would have answered (404
109
+ * missing or deleted, 415 binary, 413 too large, 502 upstream), so a client can treat both routes'
110
+ * failures alike.
111
+ */
112
+ export type ReadFilesOutcome =
113
+ | { content: string }
114
+ | { error: { status: number; code?: string; message: string } };
115
+
116
+ /**
117
+ * Answer of `POST /__studio/files/read`. `files` pairs each answered path with its outcome;
118
+ * `omitted` lists the requested paths this response did not get to (a path or byte budget ran out).
119
+ * Omission is never an error and never a 400: the client asks for those paths again. A backend
120
+ * always answers the FIRST requested path, which is what guarantees that asking again makes
121
+ * progress.
122
+ */
123
+ export interface ReadFilesResult {
124
+ files: [path: string, outcome: ReadFilesOutcome][];
125
+ omitted: string[];
126
+ }
127
+
90
128
  /**
91
129
  * What a backend will accept as an upload, when it says.
92
130
  *
@@ -772,9 +810,10 @@ export interface AiModelsResponse {
772
810
  * lapsed", and those are different sentences on screen — the second one explains a thing that
773
811
  * used to work. `cf_upstream_error` is the third case and deliberately arrives WITH `configured:
774
812
  * true`: Cloudflare being briefly unreachable is not a reason to send someone round an OAuth flow
775
- * that fixes nothing.
813
+ * that fixes nothing. `cf_account_required` is a live grant with no account chosen (ai.md §2.1):
814
+ * re-authorizing lands back in the same state, so it calls for the account picker instead.
776
815
  */
777
- code?: "cf_not_connected" | "cf_reconnect_required" | "cf_upstream_error";
816
+ code?: "cf_not_connected" | "cf_reconnect_required" | "cf_account_required" | "cf_upstream_error";
778
817
  }
779
818
 
780
819
  // ─── Cloudflare publish surface ──────────────────────────────────────────────