@statewalker/webrun-files-rpc 0.1.1 → 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/README.md CHANGED
@@ -119,6 +119,12 @@ Every argument and result is a plain value: strings, numbers, booleans, plain ob
119
119
  | `stats`, `exists` | as the `FilesApi` |
120
120
  | `mkdir(path, {requestId?})`, `remove(path, {requestId?})`, `move(source, target, {requestId?})`, `copy(source, target, {requestId?})` | as the `FilesApi`; with a `requestId`, once (a call delivered again gets the first answer) |
121
121
 
122
+ `move` is as atomic as the served backend's `move` (node, mem, sqlite: atomic; s3 or a cross-mount
123
+ composite: copy then delete); the protocol adds no guarantee of its own. A `MoveIncompleteError`
124
+ thrown by the served backend is rebuilt by the client as a `MoveIncompleteError` with the client's
125
+ paths. It is recognised by its message (`move /a -> /b: the target was replaced, but the source
126
+ could not be removed`), since a structured clone keeps an error's message but only standard names.
127
+
122
128
  - **Stream ids:** `streamId` may be chosen by the caller: 16 to 128 characters of `[A-Za-z0-9_-]`,
123
129
  random, such as `crypto.randomUUID()` without dashes. Then an open delivered twice opens one
124
130
  stream; the client always does this.
@@ -1,5 +1,47 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
- //#region src/client.ts
2
+ // ../webrun-files/dist/esm/index.js
3
+ /**
4
+ * Thrown by a `move` done as copy-then-delete when the copy succeeded but the source
5
+ * could not be removed, even on a second attempt. The move is half done: the target
6
+ * already holds the moved content (an existing target was replaced), and the source
7
+ * still exists. A `false` would wrongly say that nothing happened, so this is thrown
8
+ * instead.
9
+ *
10
+ * @example
11
+ * try {
12
+ * await files.move("/repo/index.lock", "/repo/index");
13
+ * } catch (error) {
14
+ * if (error instanceof MoveIncompleteError) await files.remove(error.source);
15
+ * else throw error;
16
+ * }
17
+ */
18
+ var MoveIncompleteError = class extends Error {
19
+ source;
20
+ target;
21
+ name = "MoveIncompleteError";
22
+ constructor(source, target, options) {
23
+ super(`move ${source} -> ${target}: the target was replaced, but the source could not be removed`, options);
24
+ this.source = source;
25
+ this.target = target;
26
+ }
27
+ };
28
+ /**
29
+ * Path manipulation utilities for virtual filesystem paths.
30
+ * All paths use forward slashes and start with "/".
31
+ */
32
+ /**
33
+ * Normalizes a file path to a consistent format.
34
+ * - Adds leading slash if missing
35
+ * - Removes trailing slash
36
+ * - Removes `.` segments
37
+ * - Collapses multiple slashes
38
+ */
39
+ function normalizePath(filePath) {
40
+ const segments = filePath.split("/").filter((s) => !!s && s !== ".");
41
+ if (segments.length === 0) return "/";
42
+ return `/${segments.join("/")}`;
43
+ }
44
+ // src/client.ts
3
45
  const toBytes$1 = (v) => v instanceof Uint8Array ? v : ArrayBuffer.isView(v) ? new Uint8Array(v.buffer, v.byteOffset, v.byteLength) : new Uint8Array(v);
4
46
  const newStreamId$1 = () => crypto.randomUUID().replace(/-/g, "");
5
47
  /** Cuts any sequence of byte arrays into chunks of exactly `size` bytes (the last may be shorter). */
@@ -155,8 +197,18 @@ var FilesRpcClient = class {
155
197
  remove(path) {
156
198
  return this.#rpc.remove(path, { requestId: newStreamId$1() });
157
199
  }
158
- move(source, target) {
159
- return this.#rpc.move(source, target, { requestId: newStreamId$1() });
200
+ /**
201
+ * One `move` call: the server's backend does the move, so it is exactly as atomic as
202
+ * that backend's `move` (node, mem, sqlite: atomic; s3 or a cross-mount composite:
203
+ * copy then delete). The client adds no guarantee, and a call that fails in transit
204
+ * leaves it unknown whether the move happened.
205
+ */
206
+ async move(source, target) {
207
+ try {
208
+ return await this.#rpc.move(source, target, { requestId: newStreamId$1() });
209
+ } catch (error) {
210
+ throw moveIncomplete(error, source, target);
211
+ }
160
212
  }
161
213
  copy(source, target) {
162
214
  return this.#rpc.copy(source, target, { requestId: newStreamId$1() });
@@ -166,8 +218,17 @@ var FilesRpcClient = class {
166
218
  function newFilesRpcClient(options) {
167
219
  return new FilesRpcClient(options);
168
220
  }
169
- //#endregion
170
- //#region src/errors.ts
221
+ /**
222
+ * A server-side `MoveIncompleteError` arrives as a plain error: over http with its name and
223
+ * message, through a structured clone (rpc transports) with its message only. Recognised by
224
+ * either, it is rebuilt with the client's paths, the original kept as its cause.
225
+ */
226
+ function moveIncomplete(error, source, target) {
227
+ const { name, message } = error ?? {};
228
+ return name === "MoveIncompleteError" || typeof message === "string" && MOVE_INCOMPLETE_MESSAGE.test(message) ? new MoveIncompleteError(normalizePath(source), normalizePath(target), { cause: error }) : error;
229
+ }
230
+ const MOVE_INCOMPLETE_MESSAGE = /^move .* -> .*: the target was replaced, but the source could not be removed$/s;
231
+ // src/errors.ts
171
232
  /**
172
233
  * An error of the protocol itself. The code also leads the message (`UNKNOWN_STREAM: …`), since
173
234
  * a structured clone keeps an error's name and message but not its other properties:
@@ -196,8 +257,7 @@ const CODES = /* @__PURE__ */ new Set([
196
257
  ]);
197
258
  /** A message led by a code, as FilesRpcError writes it (`CODE: …`). */
198
259
  const LEADING_CODE = new RegExp(`^(${[...CODES].join("|")}):`);
199
- //#endregion
200
- //#region src/keyed.ts
260
+ // src/keyed.ts
201
261
  /**
202
262
  * Values kept by key, and waiters for keys that have no value yet: what both ends of a stream
203
263
  * use to keep chunks by chunkId and wait for the ones still on their way.
@@ -260,8 +320,7 @@ var Keyed = class {
260
320
  this.#waiters.clear();
261
321
  }
262
322
  };
263
- //#endregion
264
- //#region src/server.ts
323
+ // src/server.ts
265
324
  /**
266
325
  * Whether a write's outcome is the FilesApi's alone: every chunk was handed to it (an emulated
267
326
  * append, until its write-back starts, can still be cancelled).
@@ -788,7 +847,6 @@ function newFilesRpcServer(options) {
788
847
  timer?.unref?.();
789
848
  return server;
790
849
  }
791
- //#endregion
792
850
  exports.FilesRpcClient = FilesRpcClient;
793
851
  exports.FilesRpcError = FilesRpcError;
794
852
  exports.errorCode = errorCode;
package/dist/client.d.ts CHANGED
@@ -37,6 +37,12 @@ export declare class FilesRpcClient implements FilesApi {
37
37
  stats(path: string): Promise<FileStats | undefined>;
38
38
  exists(path: string): Promise<boolean>;
39
39
  remove(path: string): Promise<boolean>;
40
+ /**
41
+ * One `move` call: the server's backend does the move, so it is exactly as atomic as
42
+ * that backend's `move` (node, mem, sqlite: atomic; s3 or a cross-mount composite:
43
+ * copy then delete). The client adds no guarantee, and a call that fails in transit
44
+ * leaves it unknown whether the move happened.
45
+ */
40
46
  move(source: string, target: string): Promise<boolean>;
41
47
  copy(source: string, target: string): Promise<boolean>;
42
48
  }
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AACnC,OAAO,KAAK,EAAE,QAAQ,EAAc,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,qBAAqB;IACpC,wFAAwF;IACxF,GAAG,EAAE,QAAQ,CAAC;IACd,8FAA8F;IAC9F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAyCD,0FAA0F;AAC1F,qBAAa,cAAe,YAAW,QAAQ;;IAO7C,YAAY,OAAO,EAAE,qBAAqB,EAMzC;IAED;;;OAGG;IACI,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,aAAa,CAAC,UAAU,CAAC,CA4C9E;IAED;;;OAGG;IACG,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC,CAEf;IAED;;;OAGG;IACG,MAAM,CACV,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC,CAEf;IAsCK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvC;IAED,6EAA6E;IACtE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,aAAa,CAAC,QAAQ,CAAC,CAY5E;IAED,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAElD;IACD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAErC;IACD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAErC;IACD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAErD;IACD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAErD;CACF;AAED,kCAAkC;AAClC,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CAEhF"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,QAAQ,EACR,SAAS,EACT,QAAQ,EACR,WAAW,EACX,WAAW,EACZ,MAAM,2BAA2B,CAAC;AAEnC,OAAO,KAAK,EAAE,QAAQ,EAAc,MAAM,YAAY,CAAC;AAEvD,MAAM,WAAW,qBAAqB;IACpC,wFAAwF;IACxF,GAAG,EAAE,QAAQ,CAAC;IACd,8FAA8F;IAC9F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,4FAA4F;IAC5F,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,qEAAqE;IACrE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,sCAAsC;IACtC,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB;AAyCD,0FAA0F;AAC1F,qBAAa,cAAe,YAAW,QAAQ;;IAO7C,YAAY,OAAO,EAAE,qBAAqB,EAMzC;IAED;;;OAGG;IACI,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,aAAa,CAAC,UAAU,CAAC,CA4C9E;IAED;;;OAGG;IACG,KAAK,CACT,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC,CAEf;IAED;;;OAGG;IACG,MAAM,CACV,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,QAAQ,CAAC,UAAU,CAAC,GAAG,aAAa,CAAC,UAAU,CAAC,GACxD,OAAO,CAAC,IAAI,CAAC,CAEf;IAsCK,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvC;IAED,6EAA6E;IACtE,IAAI,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,WAAgB,GAAG,aAAa,CAAC,QAAQ,CAAC,CAY5E;IAED,KAAK,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,SAAS,GAAG,SAAS,CAAC,CAElD;IACD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAErC;IACD,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAErC;IACD;;;;;OAKG;IACG,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAM3D;IACD,IAAI,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAErD;CACF;AAED,kCAAkC;AAClC,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,qBAAqB,GAAG,cAAc,CAEhF"}
package/dist/esm/index.js CHANGED
@@ -1,4 +1,46 @@
1
- //#region src/client.ts
1
+ // ../webrun-files/dist/esm/index.js
2
+ /**
3
+ * Thrown by a `move` done as copy-then-delete when the copy succeeded but the source
4
+ * could not be removed, even on a second attempt. The move is half done: the target
5
+ * already holds the moved content (an existing target was replaced), and the source
6
+ * still exists. A `false` would wrongly say that nothing happened, so this is thrown
7
+ * instead.
8
+ *
9
+ * @example
10
+ * try {
11
+ * await files.move("/repo/index.lock", "/repo/index");
12
+ * } catch (error) {
13
+ * if (error instanceof MoveIncompleteError) await files.remove(error.source);
14
+ * else throw error;
15
+ * }
16
+ */
17
+ var MoveIncompleteError = class extends Error {
18
+ source;
19
+ target;
20
+ name = "MoveIncompleteError";
21
+ constructor(source, target, options) {
22
+ super(`move ${source} -> ${target}: the target was replaced, but the source could not be removed`, options);
23
+ this.source = source;
24
+ this.target = target;
25
+ }
26
+ };
27
+ /**
28
+ * Path manipulation utilities for virtual filesystem paths.
29
+ * All paths use forward slashes and start with "/".
30
+ */
31
+ /**
32
+ * Normalizes a file path to a consistent format.
33
+ * - Adds leading slash if missing
34
+ * - Removes trailing slash
35
+ * - Removes `.` segments
36
+ * - Collapses multiple slashes
37
+ */
38
+ function normalizePath(filePath) {
39
+ const segments = filePath.split("/").filter((s) => !!s && s !== ".");
40
+ if (segments.length === 0) return "/";
41
+ return `/${segments.join("/")}`;
42
+ }
43
+ // src/client.ts
2
44
  const toBytes$1 = (v) => v instanceof Uint8Array ? v : ArrayBuffer.isView(v) ? new Uint8Array(v.buffer, v.byteOffset, v.byteLength) : new Uint8Array(v);
3
45
  const newStreamId$1 = () => crypto.randomUUID().replace(/-/g, "");
4
46
  /** Cuts any sequence of byte arrays into chunks of exactly `size` bytes (the last may be shorter). */
@@ -154,8 +196,18 @@ var FilesRpcClient = class {
154
196
  remove(path) {
155
197
  return this.#rpc.remove(path, { requestId: newStreamId$1() });
156
198
  }
157
- move(source, target) {
158
- return this.#rpc.move(source, target, { requestId: newStreamId$1() });
199
+ /**
200
+ * One `move` call: the server's backend does the move, so it is exactly as atomic as
201
+ * that backend's `move` (node, mem, sqlite: atomic; s3 or a cross-mount composite:
202
+ * copy then delete). The client adds no guarantee, and a call that fails in transit
203
+ * leaves it unknown whether the move happened.
204
+ */
205
+ async move(source, target) {
206
+ try {
207
+ return await this.#rpc.move(source, target, { requestId: newStreamId$1() });
208
+ } catch (error) {
209
+ throw moveIncomplete(error, source, target);
210
+ }
159
211
  }
160
212
  copy(source, target) {
161
213
  return this.#rpc.copy(source, target, { requestId: newStreamId$1() });
@@ -165,8 +217,17 @@ var FilesRpcClient = class {
165
217
  function newFilesRpcClient(options) {
166
218
  return new FilesRpcClient(options);
167
219
  }
168
- //#endregion
169
- //#region src/errors.ts
220
+ /**
221
+ * A server-side `MoveIncompleteError` arrives as a plain error: over http with its name and
222
+ * message, through a structured clone (rpc transports) with its message only. Recognised by
223
+ * either, it is rebuilt with the client's paths, the original kept as its cause.
224
+ */
225
+ function moveIncomplete(error, source, target) {
226
+ const { name, message } = error ?? {};
227
+ return name === "MoveIncompleteError" || typeof message === "string" && MOVE_INCOMPLETE_MESSAGE.test(message) ? new MoveIncompleteError(normalizePath(source), normalizePath(target), { cause: error }) : error;
228
+ }
229
+ const MOVE_INCOMPLETE_MESSAGE = /^move .* -> .*: the target was replaced, but the source could not be removed$/s;
230
+ // src/errors.ts
170
231
  /**
171
232
  * An error of the protocol itself. The code also leads the message (`UNKNOWN_STREAM: …`), since
172
233
  * a structured clone keeps an error's name and message but not its other properties:
@@ -195,8 +256,7 @@ const CODES = /* @__PURE__ */ new Set([
195
256
  ]);
196
257
  /** A message led by a code, as FilesRpcError writes it (`CODE: …`). */
197
258
  const LEADING_CODE = new RegExp(`^(${[...CODES].join("|")}):`);
198
- //#endregion
199
- //#region src/keyed.ts
259
+ // src/keyed.ts
200
260
  /**
201
261
  * Values kept by key, and waiters for keys that have no value yet: what both ends of a stream
202
262
  * use to keep chunks by chunkId and wait for the ones still on their way.
@@ -259,8 +319,7 @@ var Keyed = class {
259
319
  this.#waiters.clear();
260
320
  }
261
321
  };
262
- //#endregion
263
- //#region src/server.ts
322
+ // src/server.ts
264
323
  /**
265
324
  * Whether a write's outcome is the FilesApi's alone: every chunk was handed to it (an emulated
266
325
  * append, until its write-back starts, can still be cancelled).
@@ -787,5 +846,4 @@ function newFilesRpcServer(options) {
787
846
  timer?.unref?.();
788
847
  return server;
789
848
  }
790
- //#endregion
791
849
  export { FilesRpcClient, FilesRpcError, errorCode, newFilesRpcClient, newFilesRpcServer };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-files-rpc",
3
- "version": "0.1.1",
3
+ "version": "0.2.0",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "FilesApi over any RPC: streamed reads and writes as chunks kept and awaited by (streamId, chunkId)",
@@ -34,7 +34,7 @@
34
34
  "src"
35
35
  ],
36
36
  "dependencies": {
37
- "@statewalker/webrun-files": "^0.10.4"
37
+ "@statewalker/webrun-files": "^0.11.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^26.6.4",
@@ -42,7 +42,7 @@
42
42
  "rolldown": "^1.2.12",
43
43
  "typescript": "^7.0.2",
44
44
  "vitest": "^4.1.4 || ^5.0.3",
45
- "@statewalker/webrun-files-mem": "^0.10.4",
45
+ "@statewalker/webrun-files-mem": "^0.11.0",
46
46
  "@statewalker/webrun-files-tests": "^3.0.0"
47
47
  },
48
48
  "sideEffects": false,
package/src/client.ts CHANGED
@@ -5,6 +5,7 @@ import type {
5
5
  ListOptions,
6
6
  ReadOptions,
7
7
  } from "@statewalker/webrun-files";
8
+ import { MoveIncompleteError, normalizePath } from "@statewalker/webrun-files";
8
9
  import type { FilesRpc, ReadResult } from "./types.js";
9
10
 
10
11
  export interface FilesRpcClientOptions {
@@ -211,8 +212,18 @@ export class FilesRpcClient implements FilesApi {
211
212
  remove(path: string): Promise<boolean> {
212
213
  return this.#rpc.remove(path, { requestId: newStreamId() });
213
214
  }
214
- move(source: string, target: string): Promise<boolean> {
215
- return this.#rpc.move(source, target, { requestId: newStreamId() });
215
+ /**
216
+ * One `move` call: the server's backend does the move, so it is exactly as atomic as
217
+ * that backend's `move` (node, mem, sqlite: atomic; s3 or a cross-mount composite:
218
+ * copy then delete). The client adds no guarantee, and a call that fails in transit
219
+ * leaves it unknown whether the move happened.
220
+ */
221
+ async move(source: string, target: string): Promise<boolean> {
222
+ try {
223
+ return await this.#rpc.move(source, target, { requestId: newStreamId() });
224
+ } catch (error) {
225
+ throw moveIncomplete(error, source, target);
226
+ }
216
227
  }
217
228
  copy(source: string, target: string): Promise<boolean> {
218
229
  return this.#rpc.copy(source, target, { requestId: newStreamId() });
@@ -223,3 +234,21 @@ export class FilesRpcClient implements FilesApi {
223
234
  export function newFilesRpcClient(options: FilesRpcClientOptions): FilesRpcClient {
224
235
  return new FilesRpcClient(options);
225
236
  }
237
+
238
+ /**
239
+ * A server-side `MoveIncompleteError` arrives as a plain error: over http with its name and
240
+ * message, through a structured clone (rpc transports) with its message only. Recognised by
241
+ * either, it is rebuilt with the client's paths, the original kept as its cause.
242
+ */
243
+ function moveIncomplete(error: unknown, source: string, target: string): unknown {
244
+ const { name, message } = (error ?? {}) as { name?: unknown; message?: unknown };
245
+ const recognised =
246
+ name === "MoveIncompleteError" ||
247
+ (typeof message === "string" && MOVE_INCOMPLETE_MESSAGE.test(message));
248
+ return recognised
249
+ ? new MoveIncompleteError(normalizePath(source), normalizePath(target), { cause: error })
250
+ : error;
251
+ }
252
+
253
+ const MOVE_INCOMPLETE_MESSAGE =
254
+ /^move .* -> .*: the target was replaced, but the source could not be removed$/s;