@lmzhen/dsh-evolution-io-fs 0.0.0 → 0.24.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/lib/index.js +106 -0
- package/lib/types/index.d.ts +51 -0
- package/package.json +29 -3
package/lib/index.js
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { nodeEvolutionIo } from "@lmzhen/dsh-evolution-core";
|
|
2
|
+
//#region lib/types/index.js
|
|
3
|
+
/** Provider name in the `evolutionIo` registry. */
|
|
4
|
+
const PROVIDER_NAME = "fs";
|
|
5
|
+
/** The seam methods the platform FileSystem cannot serve yet (contract C2 fallback list). */
|
|
6
|
+
const PLATFORM_IO_FALLBACKS = [
|
|
7
|
+
"remove",
|
|
8
|
+
"rename",
|
|
9
|
+
"copy"
|
|
10
|
+
];
|
|
11
|
+
/** The two platform error codes a guarded write reports when another writer won the race. */
|
|
12
|
+
const VERSION_CONFLICT_CODES = new Set(["FS_STALE_VERSION", "FS_NOT_OBSERVED"]);
|
|
13
|
+
/** @returns whether the error is the platform's optimistic-concurrency conflict. */
|
|
14
|
+
function isVersionConflict(error) {
|
|
15
|
+
const code = error?.code;
|
|
16
|
+
return typeof code === "string" && VERSION_CONFLICT_CODES.has(code);
|
|
17
|
+
}
|
|
18
|
+
/** A missing file is `null` on this seam; a real read failure must stay an error. */
|
|
19
|
+
async function readTextThrough(fs, path) {
|
|
20
|
+
const target = await fs.resolve(path);
|
|
21
|
+
try {
|
|
22
|
+
return await fs.readText(target);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
if (await fs.stat(target).catch(() => void 0) === void 0) return null;
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
/** Build the seam implementation on top of `ctx.fs`, falling back to the node backend for the gaps. */
|
|
29
|
+
/**
|
|
30
|
+
* @param ctx - the plugin context (needs the platform `fs` service).
|
|
31
|
+
* @param options.attempts - how many times a conflicted RMW is re-run before the conflict surfaces.
|
|
32
|
+
* @returns the seam implementation backed by the platform filesystem.
|
|
33
|
+
*/
|
|
34
|
+
function platformEvolutionIo(ctx, options = {}) {
|
|
35
|
+
const attempts = options.attempts ?? 3;
|
|
36
|
+
const fs = ctx.get("fs");
|
|
37
|
+
if (fs === void 0 || typeof fs.resolve !== "function" || typeof fs.writeText !== "function") throw new Error("evolution-io-fs: the platform fs service is not mounted — the platform provider cannot be built");
|
|
38
|
+
const fallback = nodeEvolutionIo();
|
|
39
|
+
return {
|
|
40
|
+
readText: (path) => readTextThrough(fs, path),
|
|
41
|
+
async writeText(path, content) {
|
|
42
|
+
await fs.writeText(await fs.resolve(path), content);
|
|
43
|
+
},
|
|
44
|
+
async exists(path) {
|
|
45
|
+
return await fs.stat(await fs.resolve(path)).catch(() => void 0) !== void 0;
|
|
46
|
+
},
|
|
47
|
+
async size(path) {
|
|
48
|
+
const info = await fs.stat(await fs.resolve(path)).catch(() => void 0);
|
|
49
|
+
if (info === void 0) return null;
|
|
50
|
+
return typeof info.size === "number" ? info.size : null;
|
|
51
|
+
},
|
|
52
|
+
async list(path) {
|
|
53
|
+
const target = await fs.resolve(path);
|
|
54
|
+
return (await fs.listDir(target).catch(() => [])).map((entry) => entry.name);
|
|
55
|
+
},
|
|
56
|
+
async isSymlink(path) {
|
|
57
|
+
const info = await fs.lstat(path).catch(() => void 0);
|
|
58
|
+
if (info === void 0) return null;
|
|
59
|
+
return info.symlink === true;
|
|
60
|
+
},
|
|
61
|
+
async transact(path, task) {
|
|
62
|
+
const target = await fs.resolve(path);
|
|
63
|
+
for (let attempt = 1;; attempt += 1) {
|
|
64
|
+
const info = await fs.stat(target).catch(() => void 0);
|
|
65
|
+
const current = info === void 0 ? null : await readTextThrough(fs, path);
|
|
66
|
+
const next = await task(current);
|
|
67
|
+
if (next === null) {
|
|
68
|
+
await fallback.remove(path);
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (next === current) return;
|
|
72
|
+
const intent = info === void 0 ? { kind: "createIfAbsent" } : {
|
|
73
|
+
kind: "replaceIfVersion",
|
|
74
|
+
version: info.version
|
|
75
|
+
};
|
|
76
|
+
try {
|
|
77
|
+
await fs.writeText(target, next, intent);
|
|
78
|
+
return;
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (!isVersionConflict(error) || attempt >= attempts) throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
remove: (path) => fallback.remove(path),
|
|
85
|
+
rename: (path, destination) => fallback.rename(path, destination),
|
|
86
|
+
copy: (path, destination) => fallback.copy(path, destination)
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
function providerFor(ctx) {
|
|
90
|
+
return {
|
|
91
|
+
name: "fs",
|
|
92
|
+
...platformEvolutionIo(ctx)
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
const name = "evolution-io-fs";
|
|
96
|
+
const inject = ["evolutionIo"];
|
|
97
|
+
/**
|
|
98
|
+
* S0: register the platform provider as an ALTERNATIVE. No default: true — the node backend stays
|
|
99
|
+
* the default until S3 flips it, so mounting this row changes no behaviour by itself.
|
|
100
|
+
*/
|
|
101
|
+
function apply(ctx) {
|
|
102
|
+
const view = ctx;
|
|
103
|
+
ctx.effect(() => ctx.evolutionIo.registerProvider(providerFor(view)), "evolution-io-fs.provider");
|
|
104
|
+
}
|
|
105
|
+
//#endregion
|
|
106
|
+
export { PLATFORM_IO_FALLBACKS, PROVIDER_NAME, apply, inject, name, platformEvolutionIo, providerFor };
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Platform-`ctx.fs` IO provider — the seam's SECOND backend (G2 S0).
|
|
3
|
+
*
|
|
4
|
+
* Why this package exists: the family's durable IO used to be one hand-written node stack
|
|
5
|
+
* (core's `nodeEvolutionIo`) with its own lock protocol and its own atomic write. The platform
|
|
6
|
+
* already ships all of it (`ctx.fs` with per-target locking and `writeText(target, text, expected)`
|
|
7
|
+
* version CAS), and two families of implementation is exactly how invariants drift. This provider
|
|
8
|
+
* maps the family seam onto the platform one, so `evolution-io-node` becomes one of two choices
|
|
9
|
+
* instead of the only medium.
|
|
10
|
+
*
|
|
11
|
+
* S0 (this step) ADDS the provider and does NOT enable it: `apply()` registers it WITHOUT
|
|
12
|
+
* `default: true`, so `provider()` still resolves the node backend and no behaviour changes.
|
|
13
|
+
* `evolution-io-node` remains the default until S3 flips it.
|
|
14
|
+
*
|
|
15
|
+
* Gaps are declared, not hidden (contract C2): the platform `FileSystem` has no
|
|
16
|
+
* `mkdir/remove/rename/copy` (upstream request ①) and its `FsInfo` carries no wall-clock mtime
|
|
17
|
+
* (request ②). Those methods therefore FALL BACK to the node backend and are listed in
|
|
18
|
+
* {@link PLATFORM_IO_FALLBACKS}; when upstream lands them the list shrinks to empty, which is the
|
|
19
|
+
* measurable 'delete the temporary shape' condition.
|
|
20
|
+
*
|
|
21
|
+
* @module @lmzhen/dsh-evolution-io-fs
|
|
22
|
+
*/
|
|
23
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
24
|
+
import { type EvolutionIoLike } from '@lmzhen/dsh-evolution-core';
|
|
25
|
+
import type { EvolutionIo } from '@lmzhen/dsh-evolution-io';
|
|
26
|
+
/** Provider name in the `evolutionIo` registry. */
|
|
27
|
+
export declare const PROVIDER_NAME = "fs";
|
|
28
|
+
/** The seam methods the platform FileSystem cannot serve yet (contract C2 fallback list). */
|
|
29
|
+
export declare const PLATFORM_IO_FALLBACKS: readonly ["remove", "rename", "copy"];
|
|
30
|
+
/** Build the seam implementation on top of `ctx.fs`, falling back to the node backend for the gaps. */
|
|
31
|
+
/**
|
|
32
|
+
* @param ctx - the plugin context (needs the platform `fs` service).
|
|
33
|
+
* @param options.attempts - how many times a conflicted RMW is re-run before the conflict surfaces.
|
|
34
|
+
* @returns the seam implementation backed by the platform filesystem.
|
|
35
|
+
*/
|
|
36
|
+
export declare function platformEvolutionIo(ctx: {
|
|
37
|
+
get(name: string): unknown;
|
|
38
|
+
}, options?: {
|
|
39
|
+
attempts?: number;
|
|
40
|
+
}): EvolutionIoLike;
|
|
41
|
+
export declare function providerFor(ctx: {
|
|
42
|
+
get(name: string): unknown;
|
|
43
|
+
}): EvolutionIo;
|
|
44
|
+
export declare const name = "evolution-io-fs";
|
|
45
|
+
export declare const inject: string[];
|
|
46
|
+
/**
|
|
47
|
+
* S0: register the platform provider as an ALTERNATIVE. No default: true — the node backend stays
|
|
48
|
+
* the default until S3 flips it, so mounting this row changes no behaviour by itself.
|
|
49
|
+
*/
|
|
50
|
+
export declare function apply(ctx: Context): void;
|
|
51
|
+
//# sourceMappingURL=index.d.ts.map
|
package/package.json
CHANGED
|
@@ -1,9 +1,35 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lmzhen/dsh-evolution-io-fs",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"license": "MIT",
|
|
3
|
+
"description": "Platform ctx.fs IO provider (the seam's second backend) (community build)",
|
|
4
|
+
"version": "0.24.0",
|
|
6
5
|
"publishConfig": {
|
|
7
6
|
"access": "public"
|
|
7
|
+
},
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/lmzhen/dsh-evolution.git",
|
|
11
|
+
"directory": "packages/evolution-io-fs"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "lib/index.js",
|
|
15
|
+
"types": "lib/types/index.d.ts",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"types": "./lib/types/index.d.ts",
|
|
19
|
+
"default": "./lib/index.js"
|
|
20
|
+
},
|
|
21
|
+
"./package.json": "./package.json"
|
|
22
|
+
},
|
|
23
|
+
"files": [
|
|
24
|
+
"lib/*.js",
|
|
25
|
+
"lib/types/**/*.d.ts"
|
|
26
|
+
],
|
|
27
|
+
"license": "MIT",
|
|
28
|
+
"dependencies": {
|
|
29
|
+
"@lmzhen/dsh-evolution-core": "^0.24.0"
|
|
30
|
+
},
|
|
31
|
+
"peerDependencies": {
|
|
32
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
33
|
+
"@lmzhen/dsh-evolution-io": "^0.24.0"
|
|
8
34
|
}
|
|
9
35
|
}
|