@statewalker/webrun-files-rpc 0.1.2 → 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,4 +1,46 @@
1
1
  Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
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
+ }
2
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, "");
@@ -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,6 +218,16 @@ var FilesRpcClient = class {
166
218
  function newFilesRpcClient(options) {
167
219
  return new FilesRpcClient(options);
168
220
  }
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;
169
231
  // src/errors.ts
170
232
  /**
171
233
  * An error of the protocol itself. The code also leads the message (`UNKNOWN_STREAM: …`), since
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,3 +1,45 @@
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
+ }
1
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, "");
@@ -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,6 +217,16 @@ var FilesRpcClient = class {
165
217
  function newFilesRpcClient(options) {
166
218
  return new FilesRpcClient(options);
167
219
  }
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;
168
230
  // src/errors.ts
169
231
  /**
170
232
  * An error of the protocol itself. The code also leads the message (`UNKNOWN_STREAM: …`), since
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@statewalker/webrun-files-rpc",
3
- "version": "0.1.2",
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.5"
37
+ "@statewalker/webrun-files": "^0.11.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@types/node": "^26.6.4",
@@ -42,8 +42,8 @@
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-tests": "^3.0.0",
46
- "@statewalker/webrun-files-mem": "^0.10.5"
45
+ "@statewalker/webrun-files-mem": "^0.11.0",
46
+ "@statewalker/webrun-files-tests": "^3.0.0"
47
47
  },
48
48
  "sideEffects": false,
49
49
  "publishConfig": {
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;