@bandeira-tech/b3nd-save 0.8.1 → 0.13.1
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/esm/clients/save-client.d.ts +63 -29
- package/esm/clients/save-client.d.ts.map +1 -1
- package/esm/clients/save-client.js +148 -68
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/mod.d.ts +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/mod.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encrypt/mod.d.ts +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/mod.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encrypt/mod.js +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.d.ts +6 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.js +12 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/path-vector.d.ts +3 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/network/policies/path-vector.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/path-vector.js +3 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/tell-and-read.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/connection.d.ts +13 -5
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/connection.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/connection.js +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.d.ts.map +1 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/rig.d.ts +3 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/rig.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/rig.js +46 -4
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/types.d.ts +4 -4
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/types.d.ts.map +1 -0
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/types/types.d.ts +119 -80
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.d.ts.map +1 -0
- package/esm/dispatch.d.ts +116 -3
- package/esm/dispatch.d.ts.map +1 -1
- package/esm/dispatch.js +381 -5
- package/esm/elasticsearch/store.d.ts +124 -25
- package/esm/elasticsearch/store.d.ts.map +1 -1
- package/esm/elasticsearch/store.js +451 -92
- package/esm/entity-store.d.ts +1 -1
- package/esm/errors.d.ts +1 -1
- package/esm/errors.js +1 -1
- package/esm/fs/mod.d.ts +26 -0
- package/esm/fs/mod.d.ts.map +1 -1
- package/esm/fs/store.d.ts +100 -23
- package/esm/fs/store.d.ts.map +1 -1
- package/esm/fs/store.js +250 -66
- package/esm/glob.d.ts +114 -0
- package/esm/glob.d.ts.map +1 -0
- package/esm/glob.js +352 -0
- package/esm/identifiers.d.ts +18 -0
- package/esm/identifiers.d.ts.map +1 -0
- package/esm/identifiers.js +19 -0
- package/esm/indexeddb/fields.d.ts +32 -0
- package/esm/indexeddb/fields.d.ts.map +1 -0
- package/esm/indexeddb/fields.js +51 -0
- package/esm/indexeddb/store.d.ts +72 -17
- package/esm/indexeddb/store.d.ts.map +1 -1
- package/esm/indexeddb/store.js +362 -106
- package/esm/ipfs/mod.d.ts +19 -0
- package/esm/ipfs/mod.d.ts.map +1 -1
- package/esm/ipfs/mod.js +19 -0
- package/esm/ipfs/store.d.ts +82 -21
- package/esm/ipfs/store.d.ts.map +1 -1
- package/esm/ipfs/store.js +269 -68
- package/esm/json-fields.d.ts +61 -0
- package/esm/json-fields.d.ts.map +1 -0
- package/esm/json-fields.js +121 -0
- package/esm/localstorage/store.d.ts +89 -21
- package/esm/localstorage/store.d.ts.map +1 -1
- package/esm/localstorage/store.js +247 -54
- package/esm/memory/store.d.ts +35 -4
- package/esm/memory/store.d.ts.map +1 -1
- package/esm/memory/store.js +69 -47
- package/esm/mod.d.ts +2 -0
- package/esm/mod.d.ts.map +1 -1
- package/esm/mod.js +1 -0
- package/esm/mongo/fields.d.ts +53 -0
- package/esm/mongo/fields.d.ts.map +1 -0
- package/esm/mongo/fields.js +71 -0
- package/esm/mongo/mod.d.ts +19 -4
- package/esm/mongo/mod.d.ts.map +1 -1
- package/esm/mongo/mod.js +4 -2
- package/esm/mongo/store.d.ts +114 -25
- package/esm/mongo/store.d.ts.map +1 -1
- package/esm/mongo/store.js +355 -102
- package/esm/postgres/store.d.ts +50 -1
- package/esm/postgres/store.d.ts.map +1 -1
- package/esm/postgres/store.js +133 -13
- package/esm/read.d.ts +54 -7
- package/esm/read.d.ts.map +1 -1
- package/esm/read.js +141 -16
- package/esm/s3/store.d.ts +125 -23
- package/esm/s3/store.d.ts.map +1 -1
- package/esm/s3/store.js +313 -57
- package/esm/sqlite/columns.d.ts +33 -0
- package/esm/sqlite/columns.d.ts.map +1 -0
- package/esm/sqlite/columns.js +51 -0
- package/esm/sqlite/store.d.ts +73 -25
- package/esm/sqlite/store.d.ts.map +1 -1
- package/esm/sqlite/store.js +411 -107
- package/esm/types.d.ts +1 -1
- package/esm/url.d.ts +122 -19
- package/esm/url.d.ts.map +1 -1
- package/esm/url.js +218 -24
- package/esm/walk-via-ls.d.ts +122 -0
- package/esm/walk-via-ls.d.ts.map +1 -0
- package/esm/walk-via-ls.js +178 -0
- package/package.json +1 -1
- package/esm/byte-entity-shim.d.ts +0 -47
- package/esm/byte-entity-shim.d.ts.map +0 -1
- package/esm/byte-entity-shim.js +0 -138
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/mod.d.ts.map +0 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encrypt/mod.d.ts.map +0 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/path-vector.d.ts.map +0 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/connection.d.ts.map +0 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/rig.d.ts.map +0 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/types.d.ts.map +0 -1
- package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.d.ts.map +0 -1
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/mod.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/tell-and-read.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/tell-and-read.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.d.ts +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/types.js +0 -0
- /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/types/types.js +0 -0
package/esm/errors.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* one line and the error code consistent (`STORAGE_ERROR` for
|
|
9
9
|
* anything that came from the underlying driver).
|
|
10
10
|
*/
|
|
11
|
-
import { Errors } from "./deps/jsr.io/@bandeira-tech/b3nd-core/0.
|
|
11
|
+
import { Errors } from "./deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/mod.js";
|
|
12
12
|
/**
|
|
13
13
|
* Build the structured-failure half of a Store result from a thrown
|
|
14
14
|
* value. `fallback` is used when `err` isn't an `Error` (rare). `uri`
|
package/esm/fs/mod.d.ts
CHANGED
|
@@ -22,6 +22,32 @@ export interface FsExecutor {
|
|
|
22
22
|
* returned — `ls` / `count` are shallow contracts.
|
|
23
23
|
*/
|
|
24
24
|
listFiles: (dir: string) => Promise<string[]>;
|
|
25
|
+
/**
|
|
26
|
+
* Walk every *file* descendant of `dir` recursively and yield each
|
|
27
|
+
* file's path **relative to `dir`** (no leading `/`, forward-slash
|
|
28
|
+
* separated). Directories themselves MUST NOT be yielded — only file
|
|
29
|
+
* leaves. Order is unspecified; callers must sort if they need it.
|
|
30
|
+
*
|
|
31
|
+
* Contract for non-existent / empty `dir`: yield nothing (do NOT
|
|
32
|
+
* throw). This matches the existing `listFiles` convention — `fn=ls`
|
|
33
|
+
* on an empty/missing prefix returns `[]`, never an error — and the
|
|
34
|
+
* `fn=find` handler inherits the same shape.
|
|
35
|
+
*
|
|
36
|
+
* `AsyncIterable` lets backends that can stream the walk lazily
|
|
37
|
+
* (Deno's `walk`, S3's paginated `ListObjectsV2`, etc.) do so without
|
|
38
|
+
* materialising the full path list; in-memory executors can yield
|
|
39
|
+
* Map keys directly. Consumers that want a materialised array call
|
|
40
|
+
* `Array.fromAsync(executor.walkFiles(dir))`.
|
|
41
|
+
*
|
|
42
|
+
* Used by the `fn=find` handler in `FsStore` to drive recursive
|
|
43
|
+
* listings (v2 §3.3, §3.5). Stores that only need `fn=ls` / `fn=read`
|
|
44
|
+
* don't invoke this path; an executor that omits it would break
|
|
45
|
+
* `fn=find` but nothing else. (The interface marks it required so the
|
|
46
|
+
* TypeScript compiler catches the omission at the call site instead
|
|
47
|
+
* of at runtime — see the smoke-rig migration note in PR #82's
|
|
48
|
+
* follow-up.)
|
|
49
|
+
*/
|
|
50
|
+
walkFiles: (dir: string) => AsyncIterable<string>;
|
|
25
51
|
cleanup?: () => Promise<void>;
|
|
26
52
|
}
|
|
27
53
|
export { FsStore } from "./store.js";
|
package/esm/fs/mod.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/fs/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC;IAChE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C;;;;OAIG;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9C,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC"}
|
|
1
|
+
{"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/fs/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,UAAU;IACzB,QAAQ,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC;IAChE,SAAS,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAClE,UAAU,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IAC5C,MAAM,EAAE,CAAC,IAAI,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,CAAC;IAC3C;;;;OAIG;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAC9C;;;;;;;;;;;;;;;;;;;;;;;;OAwBG;IACH,SAAS,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,aAAa,CAAC,MAAM,CAAC,CAAC;IAClD,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,OAAO,EAAE,OAAO,EAAE,MAAM,YAAY,CAAC"}
|
package/esm/fs/store.d.ts
CHANGED
|
@@ -1,45 +1,122 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FsStore — Filesystem implementation of
|
|
2
|
+
* FsStore — Filesystem implementation of `EntityStore`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The store is naive: URIs it is handed ARE relative filesystem paths
|
|
5
|
+
* under `rootDir`, and record payloads ARE the bytes written to those
|
|
6
|
+
* paths. No URI rewriting, no bytes/typed bifurcation, no field
|
|
7
|
+
* planning. Only `BYTES_ENTITY` is supported — any other schema is
|
|
8
|
+
* rejected at `entitySupport` time. Callers that want typed storage
|
|
9
|
+
* or a custom on-disk shape supply that via a `SaveClient` mapper.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
+
* ## Lifecycle
|
|
12
|
+
*
|
|
13
|
+
* `entitySupport(schema)` is pure — it validates the schema is
|
|
14
|
+
* `BYTES_ENTITY` and returns the operational handle (root path,
|
|
15
|
+
* signature for collision detection).
|
|
16
|
+
* `provisionEntity(meta)` writes a tiny meta file at
|
|
17
|
+
* `{rootDir}/.b3nd/entities/{name}` holding the signature; subsequent
|
|
18
|
+
* `entityStatus(meta)` reads it back. Same-name different-shape
|
|
19
|
+
* provisioning throws. The bookkeeping dir is dot-prefixed so an app
|
|
20
|
+
* that points `rootDir` straight at a tree it also serves/walks keeps
|
|
21
|
+
* the store's metadata out of the way — conventional dotfile-skipping
|
|
22
|
+
* walkers ignore it, and it won't collide with content.
|
|
23
|
+
*
|
|
24
|
+
* ## Read params
|
|
25
|
+
*
|
|
26
|
+
* `fn=ls` / `fn=count` (without a glob) are shallow direct-leaves only:
|
|
27
|
+
* `listFiles` on the prefix's directory (== `{rootDir}/{prefixUri}`),
|
|
28
|
+
* filter to direct-child files (no further `/`), apply standard
|
|
29
|
+
* params (`limit`/`page`/`sortBy=uri`/`sortOrder`/`format`) in memory
|
|
30
|
+
* before opening file streams.
|
|
31
|
+
*
|
|
32
|
+
* `fn=find` is the recursive variant (v2 spec §3.3, §3.5): the store
|
|
33
|
+
* calls `executor.walkFiles` on the prefix directory and yields the
|
|
34
|
+
* full subtree as URIs. The glob post-filter, pagination, sort, and
|
|
35
|
+
* cursor slot are all applied by `dispatchRead` (`pushDownFind: false`).
|
|
36
|
+
*
|
|
37
|
+
* ## URI safety
|
|
38
|
+
*
|
|
39
|
+
* The store rejects URIs containing `..` segments or leading `/` at
|
|
40
|
+
* write/read/delete time — everything else is treated as an opaque
|
|
41
|
+
* relative path. The caller's mapper is responsible for producing
|
|
42
|
+
* path-safe URIs; the store's validation is a defense-in-depth check,
|
|
43
|
+
* not a sanitizer.
|
|
11
44
|
*/
|
|
12
|
-
import type { DeleteResult, Output, StatusResult } from "../deps/jsr.io/@bandeira-tech/b3nd-core/0.
|
|
13
|
-
import { type BytesOnlyEntityMeta } from "../byte-entity-shim.js";
|
|
45
|
+
import type { DeleteResult, Output, StatusResult } from "../deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.js";
|
|
14
46
|
import type { EntityStore } from "../entity-store.js";
|
|
15
|
-
import type
|
|
47
|
+
import { type EntityMeta, type EntityRecord, type EntitySchema } from "../entity.js";
|
|
16
48
|
import type { StoreCapabilities, StoreWriteResult } from "../types.js";
|
|
17
49
|
import type { FsExecutor } from "./mod.js";
|
|
18
|
-
|
|
50
|
+
/**
|
|
51
|
+
* FsStore-specific entity handle.
|
|
52
|
+
*
|
|
53
|
+
* `signature` is used for collision detection at provision time. All
|
|
54
|
+
* FsStore metas today have the same signature (only BYTES_ENTITY is
|
|
55
|
+
* supported), but the field is kept so future changes to the on-disk
|
|
56
|
+
* meta shape trigger the same collision path.
|
|
57
|
+
*/
|
|
58
|
+
export interface FsEntityMeta extends EntityMeta {
|
|
59
|
+
readonly signature: string;
|
|
60
|
+
}
|
|
61
|
+
export declare class FsStore implements EntityStore<FsEntityMeta> {
|
|
19
62
|
private readonly rootDir;
|
|
20
63
|
private readonly executor;
|
|
21
64
|
constructor(rootDir: string, executor: FsExecutor);
|
|
22
|
-
entitySupport(schema: EntitySchema):
|
|
23
|
-
entityStatus(meta:
|
|
24
|
-
provisionEntity(
|
|
25
|
-
write(meta:
|
|
65
|
+
entitySupport(schema: EntitySchema): FsEntityMeta;
|
|
66
|
+
entityStatus(meta: FsEntityMeta): Promise<"live" | "unprovisioned">;
|
|
67
|
+
provisionEntity(meta: FsEntityMeta): Promise<void>;
|
|
68
|
+
write(meta: FsEntityMeta, entries: {
|
|
26
69
|
uri: string;
|
|
27
70
|
record: EntityRecord;
|
|
28
71
|
}[]): Promise<StoreWriteResult[]>;
|
|
29
|
-
read<T = EntityRecord | undefined>(
|
|
30
|
-
delete(meta: BytesOnlyEntityMeta, uris: string[]): Promise<DeleteResult[]>;
|
|
31
|
-
private _writeBytes;
|
|
32
|
-
private _readBytes;
|
|
72
|
+
read<T = EntityRecord | undefined>(_meta: FsEntityMeta, urls: string[]): Promise<Output<T>[]>;
|
|
33
73
|
private _readOne;
|
|
34
|
-
/** Resolve the on-disk directory that holds the direct leaves of `prefixUri`. */
|
|
35
|
-
private _dirForPrefix;
|
|
36
74
|
/** List direct-child URIs under a prefix (no recursion). */
|
|
37
75
|
private _listChildUris;
|
|
76
|
+
/**
|
|
77
|
+
* List every descendant URI under a prefix (deep, recursive). Drives
|
|
78
|
+
* `fn=find` — `dispatchRead` then applies the glob, sort, cursor, and
|
|
79
|
+
* pagination on the result.
|
|
80
|
+
*
|
|
81
|
+
* Yields URIs as they come out of `executor.walkFiles`. Order is
|
|
82
|
+
* whatever the executor produces (Deno's `walk` is unspecified);
|
|
83
|
+
* dispatch's `sortBy=uri` / `sortBy=leaf` paths cover ordering when
|
|
84
|
+
* the caller asks for it.
|
|
85
|
+
*/
|
|
86
|
+
private _listAllDescendantUris;
|
|
38
87
|
private _ls;
|
|
88
|
+
/**
|
|
89
|
+
* `fn=find` handler: deep walk under the prefix, return Output[] or
|
|
90
|
+
* string[] depending on `format`. dispatch runs the glob post-filter,
|
|
91
|
+
* cursor slot, and pagination on top.
|
|
92
|
+
*
|
|
93
|
+
* `sortBy=uri` is handled here (the dispatch contract is: every
|
|
94
|
+
* backend handles native uri-sort itself; dispatch only post-sorts
|
|
95
|
+
* non-uri sortBy values). Without this, Deno's `walk` yields entries
|
|
96
|
+
* in directory-traversal order, which is unstable across platforms
|
|
97
|
+
* and not what `sortBy=uri` callers expect.
|
|
98
|
+
*/
|
|
99
|
+
private _find;
|
|
39
100
|
private _count;
|
|
40
|
-
|
|
101
|
+
delete(meta: FsEntityMeta, uris: string[]): Promise<DeleteResult[]>;
|
|
41
102
|
status(): Promise<StatusResult>;
|
|
42
103
|
capabilities(): StoreCapabilities;
|
|
43
|
-
|
|
104
|
+
/** Full on-disk path for a record's URI. */
|
|
105
|
+
private _dataPath;
|
|
106
|
+
/** Directory under which direct-leaf records of a prefix URI live. */
|
|
107
|
+
private _dirForPrefix;
|
|
108
|
+
/** Full on-disk path for an entity's meta record. */
|
|
109
|
+
private _metaPath;
|
|
110
|
+
/**
|
|
111
|
+
* Read the persisted signature for an entity, or `null` if no
|
|
112
|
+
* provisioning record exists. Used by both `entityStatus` and the
|
|
113
|
+
* write/delete provisioning gate.
|
|
114
|
+
*/
|
|
115
|
+
private _readMetaSignature;
|
|
116
|
+
/**
|
|
117
|
+
* Scan the meta directory for every provisioned entity name. Used by
|
|
118
|
+
* `status()` to advertise the entities a peer can talk to.
|
|
119
|
+
*/
|
|
120
|
+
private _listProvisionedEntities;
|
|
44
121
|
}
|
|
45
122
|
//# sourceMappingURL=store.d.ts.map
|
package/esm/fs/store.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/fs/store.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/fs/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2CG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,MAAM,EACN,YAAY,EACb,MAAM,mEAAmE,CAAC;AAK3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AACtB,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAM3C;;;;;;;GAOG;AACH,MAAM,WAAW,YAAa,SAAQ,UAAU;IAC9C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAwBD,qBAAa,OAAQ,YAAW,WAAW,CAAC,YAAY,CAAC;IACvD,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAS;IACjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAa;gBAE1B,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,UAAU;IAUjD,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,YAAY;IAiB3C,YAAY,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,MAAM,GAAG,eAAe,CAAC;IAMnE,eAAe,CAAC,IAAI,EAAE,YAAY,GAAG,OAAO,CAAC,IAAI,CAAC;IAoBlD,KAAK,CACT,IAAI,EAAE,YAAY,EAClB,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,EAAE,GAC/C,OAAO,CAAC,gBAAgB,EAAE,CAAC;IA2C9B,IAAI,CAAC,CAAC,GAAG,YAAY,GAAG,SAAS,EAC/B,KAAK,EAAE,YAAY,EACnB,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YAeT,QAAQ;IAUtB,4DAA4D;YAC9C,cAAc;IAc5B;;;;;;;;;OASG;YACW,sBAAsB;YAetB,GAAG;IA6BjB;;;;;;;;;;OAUG;YACW,KAAK;YAuBL,MAAM;IAcd,MAAM,CAAC,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAiCnE,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;IA2BrC,YAAY,IAAI,iBAAiB;IAMjC,4CAA4C;IAC5C,OAAO,CAAC,SAAS;IAIjB,sEAAsE;IACtE,OAAO,CAAC,aAAa;IAKrB,qDAAqD;IACrD,OAAO,CAAC,SAAS;IAIjB;;;;OAIG;YACW,kBAAkB;IAchC;;;OAGG;YACW,wBAAwB;CAUvC"}
|
package/esm/fs/store.js
CHANGED
|
@@ -1,39 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* FsStore — Filesystem implementation of
|
|
2
|
+
* FsStore — Filesystem implementation of `EntityStore`.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
4
|
+
* The store is naive: URIs it is handed ARE relative filesystem paths
|
|
5
|
+
* under `rootDir`, and record payloads ARE the bytes written to those
|
|
6
|
+
* paths. No URI rewriting, no bytes/typed bifurcation, no field
|
|
7
|
+
* planning. Only `BYTES_ENTITY` is supported — any other schema is
|
|
8
|
+
* rejected at `entitySupport` time. Callers that want typed storage
|
|
9
|
+
* or a custom on-disk shape supply that via a `SaveClient` mapper.
|
|
7
10
|
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
+
* ## Lifecycle
|
|
12
|
+
*
|
|
13
|
+
* `entitySupport(schema)` is pure — it validates the schema is
|
|
14
|
+
* `BYTES_ENTITY` and returns the operational handle (root path,
|
|
15
|
+
* signature for collision detection).
|
|
16
|
+
* `provisionEntity(meta)` writes a tiny meta file at
|
|
17
|
+
* `{rootDir}/.b3nd/entities/{name}` holding the signature; subsequent
|
|
18
|
+
* `entityStatus(meta)` reads it back. Same-name different-shape
|
|
19
|
+
* provisioning throws. The bookkeeping dir is dot-prefixed so an app
|
|
20
|
+
* that points `rootDir` straight at a tree it also serves/walks keeps
|
|
21
|
+
* the store's metadata out of the way — conventional dotfile-skipping
|
|
22
|
+
* walkers ignore it, and it won't collide with content.
|
|
23
|
+
*
|
|
24
|
+
* ## Read params
|
|
25
|
+
*
|
|
26
|
+
* `fn=ls` / `fn=count` (without a glob) are shallow direct-leaves only:
|
|
27
|
+
* `listFiles` on the prefix's directory (== `{rootDir}/{prefixUri}`),
|
|
28
|
+
* filter to direct-child files (no further `/`), apply standard
|
|
29
|
+
* params (`limit`/`page`/`sortBy=uri`/`sortOrder`/`format`) in memory
|
|
30
|
+
* before opening file streams.
|
|
31
|
+
*
|
|
32
|
+
* `fn=find` is the recursive variant (v2 spec §3.3, §3.5): the store
|
|
33
|
+
* calls `executor.walkFiles` on the prefix directory and yields the
|
|
34
|
+
* full subtree as URIs. The glob post-filter, pagination, sort, and
|
|
35
|
+
* cursor slot are all applied by `dispatchRead` (`pushDownFind: false`).
|
|
36
|
+
*
|
|
37
|
+
* ## URI safety
|
|
38
|
+
*
|
|
39
|
+
* The store rejects URIs containing `..` segments or leading `/` at
|
|
40
|
+
* write/read/delete time — everything else is treated as an opaque
|
|
41
|
+
* relative path. The caller's mapper is responsible for producing
|
|
42
|
+
* path-safe URIs; the store's validation is a defense-in-depth check,
|
|
43
|
+
* not a sanitizer.
|
|
11
44
|
*/
|
|
12
|
-
import { bytesOnlyDelete, bytesOnlyEntitySupport, bytesOnlyRead, bytesOnlyWrite, } from "../byte-entity-shim.js";
|
|
13
45
|
import { dispatchRead } from "../dispatch.js";
|
|
14
46
|
import { storageFailure } from "../errors.js";
|
|
15
47
|
import { validateReadParams } from "../read.js";
|
|
48
|
+
import { BYTES_ENTITY, } from "../entity.js";
|
|
16
49
|
const STORE_NAME = "FsStore";
|
|
17
|
-
const
|
|
18
|
-
|
|
19
|
-
* Convert a URI to a relative filesystem path.
|
|
20
|
-
* `protocol://host/path` becomes `protocol_host/path.bin`.
|
|
21
|
-
*
|
|
22
|
-
* Only the FIRST `://` is rewritten — embedded `://` (rare but legal
|
|
23
|
-
* in protocol-tunneling uris) stays in the path.
|
|
24
|
-
*/
|
|
25
|
-
function uriToRelPath(uri) {
|
|
26
|
-
return uri.replace("://", "_") + EXT;
|
|
27
|
-
}
|
|
50
|
+
const META_DIR = ".b3nd/entities";
|
|
51
|
+
const BYTES_SIGNATURE = "bytes:v1";
|
|
28
52
|
/**
|
|
29
|
-
*
|
|
30
|
-
* `
|
|
53
|
+
* Validate that a URI is safe to use as a relative filesystem path
|
|
54
|
+
* under `rootDir`. Rejects `..` segments (path traversal) and leading
|
|
55
|
+
* `/` (absolute paths). Everything else is passed through as-is —
|
|
56
|
+
* mappers own path shape.
|
|
31
57
|
*/
|
|
32
|
-
function
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
58
|
+
function assertPathSafe(uri) {
|
|
59
|
+
if (uri.startsWith("/")) {
|
|
60
|
+
throw new Error(`${STORE_NAME}: URI must not start with '/'; got '${uri}'`);
|
|
61
|
+
}
|
|
62
|
+
const parts = uri.split("/");
|
|
63
|
+
for (const p of parts) {
|
|
64
|
+
if (p === "..") {
|
|
65
|
+
throw new Error(`${STORE_NAME}: URI must not contain '..' segments; got '${uri}'`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
37
68
|
}
|
|
38
69
|
export class FsStore {
|
|
39
70
|
rootDir;
|
|
@@ -46,64 +77,98 @@ export class FsStore {
|
|
|
46
77
|
this.rootDir = rootDir.replace(/\/+$/, "");
|
|
47
78
|
this.executor = executor;
|
|
48
79
|
}
|
|
49
|
-
// ──
|
|
80
|
+
// ── Lifecycle ────────────────────────────────────────────────────
|
|
50
81
|
entitySupport(schema) {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
82
|
+
if (schema.name !== BYTES_ENTITY.name) {
|
|
83
|
+
throw new Error(`${STORE_NAME}: only BYTES_ENTITY is supported. Compose a ` +
|
|
84
|
+
`SaveClient mapper to encode typed records to bytes upstream.`);
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
support: {
|
|
88
|
+
entity: schema.name,
|
|
89
|
+
supported: ["payload"],
|
|
90
|
+
unsupported: [],
|
|
91
|
+
},
|
|
92
|
+
signature: BYTES_SIGNATURE,
|
|
93
|
+
};
|
|
61
94
|
}
|
|
62
|
-
|
|
63
|
-
|
|
95
|
+
async entityStatus(meta) {
|
|
96
|
+
const recorded = await this._readMetaSignature(meta.support.entity);
|
|
97
|
+
if (recorded === null)
|
|
98
|
+
return "unprovisioned";
|
|
99
|
+
return recorded === meta.signature ? "live" : "unprovisioned";
|
|
64
100
|
}
|
|
65
|
-
|
|
66
|
-
|
|
101
|
+
async provisionEntity(meta) {
|
|
102
|
+
const recorded = await this._readMetaSignature(meta.support.entity);
|
|
103
|
+
if (recorded !== null) {
|
|
104
|
+
if (recorded !== meta.signature) {
|
|
105
|
+
throw new Error(`${STORE_NAME}: entity '${meta.support.entity}' is already ` +
|
|
106
|
+
`provisioned with a different shape`);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const path = this._metaPath(meta.support.entity);
|
|
111
|
+
await this.executor.writeFile(path, new TextEncoder().encode(meta.signature));
|
|
67
112
|
}
|
|
68
|
-
// ──
|
|
69
|
-
async
|
|
113
|
+
// ── Write ────────────────────────────────────────────────────────
|
|
114
|
+
async write(meta, entries) {
|
|
115
|
+
if (entries.length === 0)
|
|
116
|
+
return [];
|
|
117
|
+
if ((await this._readMetaSignature(meta.support.entity)) !== meta.signature) {
|
|
118
|
+
return entries.map(({ uri }) => ({
|
|
119
|
+
success: false,
|
|
120
|
+
...storageFailure(new Error(`${STORE_NAME}: entity '${meta.support.entity}' is not provisioned`), "Entity not provisioned", uri),
|
|
121
|
+
}));
|
|
122
|
+
}
|
|
70
123
|
const results = [];
|
|
71
|
-
for (const
|
|
124
|
+
for (const { uri, record } of entries) {
|
|
72
125
|
try {
|
|
73
|
-
|
|
126
|
+
assertPathSafe(uri);
|
|
127
|
+
const payload = record.payload;
|
|
128
|
+
if (!(payload instanceof Uint8Array) &&
|
|
129
|
+
!(payload instanceof ReadableStream)) {
|
|
130
|
+
throw new Error("BYTES_ENTITY record.payload must be Uint8Array or ReadableStream");
|
|
131
|
+
}
|
|
132
|
+
await this.executor.writeFile(this._dataPath(uri), payload);
|
|
74
133
|
results.push({ success: true });
|
|
75
134
|
}
|
|
76
135
|
catch (err) {
|
|
77
136
|
results.push({
|
|
78
137
|
success: false,
|
|
79
|
-
...storageFailure(err, "Write failed",
|
|
138
|
+
...storageFailure(err, "Write failed", uri),
|
|
80
139
|
});
|
|
81
140
|
}
|
|
82
141
|
}
|
|
83
142
|
return results;
|
|
84
143
|
}
|
|
85
|
-
|
|
144
|
+
// ── Read ─────────────────────────────────────────────────────────
|
|
145
|
+
read(_meta, urls) {
|
|
86
146
|
return dispatchRead(urls, STORE_NAME, {
|
|
87
147
|
read: (p) => this._readOne(p.uri),
|
|
88
148
|
ls: (p) => this._ls(p),
|
|
89
149
|
count: (p) => this._count(p),
|
|
150
|
+
find: (p) => this._find(p),
|
|
151
|
+
// The fs handler does not interpret the glob — it returns every
|
|
152
|
+
// descendant URI; dispatch's `compileSaveGlob` post-filter does
|
|
153
|
+
// the matching. Keeping the executor interface free of glob
|
|
154
|
+
// semantics (Deno's `walk` has its own glob notion that does NOT
|
|
155
|
+
// match the §3.3 grammar). See PR design note.
|
|
156
|
+
pushDownFind: false,
|
|
90
157
|
});
|
|
91
158
|
}
|
|
92
159
|
async _readOne(uri) {
|
|
93
160
|
try {
|
|
94
|
-
|
|
161
|
+
assertPathSafe(uri);
|
|
162
|
+
const stream = await this.executor.readFile(this._dataPath(uri));
|
|
163
|
+
return { payload: stream };
|
|
95
164
|
}
|
|
96
165
|
catch {
|
|
97
166
|
return undefined;
|
|
98
167
|
}
|
|
99
168
|
}
|
|
100
|
-
/** Resolve the on-disk directory that holds the direct leaves of `prefixUri`. */
|
|
101
|
-
_dirForPrefix(prefixUri) {
|
|
102
|
-
const rel = uriToRelPath(prefixUri).slice(0, -EXT.length).replace(/\/+$/, "");
|
|
103
|
-
return rel ? `${this.rootDir}/${rel}` : this.rootDir;
|
|
104
|
-
}
|
|
105
169
|
/** List direct-child URIs under a prefix (no recursion). */
|
|
106
170
|
async _listChildUris(prefixUri) {
|
|
171
|
+
assertPathSafe(prefixUri);
|
|
107
172
|
let files;
|
|
108
173
|
try {
|
|
109
174
|
files = await this.executor.listFiles(this._dirForPrefix(prefixUri));
|
|
@@ -111,10 +176,35 @@ export class FsStore {
|
|
|
111
176
|
catch {
|
|
112
177
|
return [];
|
|
113
178
|
}
|
|
114
|
-
const
|
|
179
|
+
const base = prefixUri.replace(/\/+$/, "");
|
|
115
180
|
return files
|
|
116
|
-
.filter((f) =>
|
|
117
|
-
.map((f) =>
|
|
181
|
+
.filter((f) => !f.includes("/"))
|
|
182
|
+
.map((f) => (base ? `${base}/${f}` : f));
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* List every descendant URI under a prefix (deep, recursive). Drives
|
|
186
|
+
* `fn=find` — `dispatchRead` then applies the glob, sort, cursor, and
|
|
187
|
+
* pagination on the result.
|
|
188
|
+
*
|
|
189
|
+
* Yields URIs as they come out of `executor.walkFiles`. Order is
|
|
190
|
+
* whatever the executor produces (Deno's `walk` is unspecified);
|
|
191
|
+
* dispatch's `sortBy=uri` / `sortBy=leaf` paths cover ordering when
|
|
192
|
+
* the caller asks for it.
|
|
193
|
+
*/
|
|
194
|
+
async _listAllDescendantUris(prefixUri) {
|
|
195
|
+
assertPathSafe(prefixUri);
|
|
196
|
+
const dir = this._dirForPrefix(prefixUri);
|
|
197
|
+
const base = prefixUri.replace(/\/+$/, "");
|
|
198
|
+
const out = [];
|
|
199
|
+
try {
|
|
200
|
+
for await (const relPath of this.executor.walkFiles(dir)) {
|
|
201
|
+
out.push(base ? `${base}/${relPath}` : relPath);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
return [];
|
|
206
|
+
}
|
|
207
|
+
return out;
|
|
118
208
|
}
|
|
119
209
|
async _ls(parsed) {
|
|
120
210
|
validateReadParams(parsed.params, STORE_NAME);
|
|
@@ -134,32 +224,79 @@ export class FsStore {
|
|
|
134
224
|
return uris;
|
|
135
225
|
const out = [];
|
|
136
226
|
for (const uri of uris) {
|
|
137
|
-
|
|
227
|
+
const rec = await this._readOne(uri);
|
|
228
|
+
if (rec !== undefined)
|
|
229
|
+
out.push([uri, rec]);
|
|
230
|
+
}
|
|
231
|
+
return out;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* `fn=find` handler: deep walk under the prefix, return Output[] or
|
|
235
|
+
* string[] depending on `format`. dispatch runs the glob post-filter,
|
|
236
|
+
* cursor slot, and pagination on top.
|
|
237
|
+
*
|
|
238
|
+
* `sortBy=uri` is handled here (the dispatch contract is: every
|
|
239
|
+
* backend handles native uri-sort itself; dispatch only post-sorts
|
|
240
|
+
* non-uri sortBy values). Without this, Deno's `walk` yields entries
|
|
241
|
+
* in directory-traversal order, which is unstable across platforms
|
|
242
|
+
* and not what `sortBy=uri` callers expect.
|
|
243
|
+
*/
|
|
244
|
+
async _find(parsed) {
|
|
245
|
+
validateReadParams(parsed.params, STORE_NAME);
|
|
246
|
+
const { params } = parsed;
|
|
247
|
+
const format = params.format ?? "full";
|
|
248
|
+
let uris = await this._listAllDescendantUris(parsed.uri);
|
|
249
|
+
if (params.sortBy === "uri") {
|
|
250
|
+
const dir = params.sortOrder === "desc" ? -1 : 1;
|
|
251
|
+
uris = [...uris].sort((a, b) => a.localeCompare(b) * dir);
|
|
252
|
+
}
|
|
253
|
+
if (format === "uris")
|
|
254
|
+
return uris;
|
|
255
|
+
const out = [];
|
|
256
|
+
for (const uri of uris) {
|
|
257
|
+
const rec = await this._readOne(uri);
|
|
258
|
+
if (rec !== undefined)
|
|
259
|
+
out.push([uri, rec]);
|
|
138
260
|
}
|
|
139
261
|
return out;
|
|
140
262
|
}
|
|
141
263
|
async _count(parsed) {
|
|
142
264
|
if (parsed.params.pattern !== undefined) {
|
|
265
|
+
// Filtered count routes through dispatch's ls-and-count path,
|
|
266
|
+
// which strips `pattern` before calling our `ls` handler — we
|
|
267
|
+
// should never see `params.pattern` here. Throw loudly if we do:
|
|
268
|
+
// it means dispatch's contract changed and the post-filter is
|
|
269
|
+
// about to silently undercount.
|
|
143
270
|
throw new Error(`${STORE_NAME}: pattern filter not supported`);
|
|
144
271
|
}
|
|
145
272
|
return (await this._listChildUris(parsed.uri)).length;
|
|
146
273
|
}
|
|
147
|
-
|
|
274
|
+
// ── Delete ───────────────────────────────────────────────────────
|
|
275
|
+
async delete(meta, uris) {
|
|
276
|
+
if (uris.length === 0)
|
|
277
|
+
return [];
|
|
278
|
+
if ((await this._readMetaSignature(meta.support.entity)) !== meta.signature) {
|
|
279
|
+
return uris.map((uri) => ({
|
|
280
|
+
success: false,
|
|
281
|
+
...storageFailure(new Error(`${STORE_NAME}: entity '${meta.support.entity}' is not provisioned`), "Entity not provisioned", uri),
|
|
282
|
+
}));
|
|
283
|
+
}
|
|
148
284
|
const results = [];
|
|
149
285
|
for (const uri of uris) {
|
|
150
286
|
try {
|
|
151
|
-
|
|
287
|
+
assertPathSafe(uri);
|
|
288
|
+
await this.executor.removeFile(this._dataPath(uri));
|
|
152
289
|
results.push({ success: true });
|
|
153
290
|
}
|
|
154
291
|
catch {
|
|
155
292
|
// File may not exist — succeed silently (matches the
|
|
156
|
-
// miss-is-not-an-error convention)
|
|
293
|
+
// miss-is-not-an-error convention).
|
|
157
294
|
results.push({ success: true });
|
|
158
295
|
}
|
|
159
296
|
}
|
|
160
297
|
return results;
|
|
161
298
|
}
|
|
162
|
-
// ── Status
|
|
299
|
+
// ── Status / capabilities ────────────────────────────────────────
|
|
163
300
|
async status() {
|
|
164
301
|
try {
|
|
165
302
|
const rootExists = await this.executor.exists(this.rootDir);
|
|
@@ -167,13 +304,15 @@ export class FsStore {
|
|
|
167
304
|
return {
|
|
168
305
|
status: "unhealthy",
|
|
169
306
|
message: `Root directory not found: ${this.rootDir}`,
|
|
170
|
-
fns: ["read", "ls", "count"],
|
|
307
|
+
fns: ["read", "ls", "find", "count"],
|
|
171
308
|
};
|
|
172
309
|
}
|
|
310
|
+
const schema = await this._listProvisionedEntities();
|
|
173
311
|
return {
|
|
174
312
|
status: "healthy",
|
|
175
313
|
message: "Filesystem store is operational",
|
|
176
|
-
|
|
314
|
+
schema: schema.map((s) => `entity:${s}`),
|
|
315
|
+
fns: ["read", "ls", "find", "count"],
|
|
177
316
|
details: { rootDir: this.rootDir },
|
|
178
317
|
};
|
|
179
318
|
}
|
|
@@ -181,14 +320,59 @@ export class FsStore {
|
|
|
181
320
|
return {
|
|
182
321
|
status: "unhealthy",
|
|
183
322
|
message: error instanceof Error ? error.message : String(error),
|
|
184
|
-
fns: ["read", "ls", "count"],
|
|
323
|
+
fns: ["read", "ls", "find", "count"],
|
|
185
324
|
};
|
|
186
325
|
}
|
|
187
326
|
}
|
|
188
327
|
capabilities() {
|
|
189
328
|
return { atomicBatch: false };
|
|
190
329
|
}
|
|
191
|
-
|
|
192
|
-
|
|
330
|
+
// ── Internals ────────────────────────────────────────────────────
|
|
331
|
+
/** Full on-disk path for a record's URI. */
|
|
332
|
+
_dataPath(uri) {
|
|
333
|
+
return `${this.rootDir}/${uri}`;
|
|
334
|
+
}
|
|
335
|
+
/** Directory under which direct-leaf records of a prefix URI live. */
|
|
336
|
+
_dirForPrefix(prefixUri) {
|
|
337
|
+
const rel = prefixUri.replace(/\/+$/, "");
|
|
338
|
+
return rel ? `${this.rootDir}/${rel}` : this.rootDir;
|
|
339
|
+
}
|
|
340
|
+
/** Full on-disk path for an entity's meta record. */
|
|
341
|
+
_metaPath(entityName) {
|
|
342
|
+
return `${this.rootDir}/${META_DIR}/${entityName}`;
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Read the persisted signature for an entity, or `null` if no
|
|
346
|
+
* provisioning record exists. Used by both `entityStatus` and the
|
|
347
|
+
* write/delete provisioning gate.
|
|
348
|
+
*/
|
|
349
|
+
async _readMetaSignature(entityName) {
|
|
350
|
+
const path = this._metaPath(entityName);
|
|
351
|
+
try {
|
|
352
|
+
if (!(await this.executor.exists(path)))
|
|
353
|
+
return null;
|
|
354
|
+
const stream = await this.executor.readFile(path);
|
|
355
|
+
const bytes = await new Response(stream).bytes();
|
|
356
|
+
return new TextDecoder().decode(bytes);
|
|
357
|
+
}
|
|
358
|
+
catch {
|
|
359
|
+
return null;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Scan the meta directory for every provisioned entity name. Used by
|
|
364
|
+
* `status()` to advertise the entities a peer can talk to.
|
|
365
|
+
*/
|
|
366
|
+
async _listProvisionedEntities() {
|
|
367
|
+
const dir = `${this.rootDir}/${META_DIR}`;
|
|
368
|
+
try {
|
|
369
|
+
if (!(await this.executor.exists(dir)))
|
|
370
|
+
return [];
|
|
371
|
+
const files = await this.executor.listFiles(dir);
|
|
372
|
+
return files.filter((f) => !f.includes("/"));
|
|
373
|
+
}
|
|
374
|
+
catch {
|
|
375
|
+
return [];
|
|
376
|
+
}
|
|
193
377
|
}
|
|
194
378
|
}
|