@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.
Files changed (161) hide show
  1. package/esm/clients/save-client.d.ts +63 -29
  2. package/esm/clients/save-client.d.ts.map +1 -1
  3. package/esm/clients/save-client.js +148 -68
  4. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/mod.d.ts +1 -1
  5. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/mod.d.ts.map +1 -0
  6. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.d.ts.map +1 -1
  7. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.d.ts.map +1 -1
  8. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encrypt/mod.d.ts +1 -0
  9. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/mod.d.ts.map +1 -0
  10. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encrypt/mod.js +1 -0
  11. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.d.ts +6 -0
  12. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.d.ts.map +1 -0
  13. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/encrypt/utils.js +12 -0
  14. 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
  15. 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
  16. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.d.ts.map +1 -1
  17. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.d.ts.map +1 -1
  18. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.d.ts.map +1 -1
  19. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.d.ts.map +1 -1
  20. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/path-vector.d.ts +3 -1
  21. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/network/policies/path-vector.d.ts.map +1 -0
  22. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/path-vector.js +3 -1
  23. 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
  24. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.d.ts.map +1 -1
  25. 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
  26. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/connection.d.ts +13 -5
  27. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/connection.d.ts.map +1 -0
  28. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/connection.js +1 -1
  29. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.d.ts.map +1 -1
  30. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.d.ts.map +1 -1
  31. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.d.ts.map +1 -1
  32. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.d.ts.map +1 -1
  33. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.d.ts.map +1 -1
  34. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/rig.d.ts +3 -0
  35. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/rig.d.ts.map +1 -0
  36. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/rig.js +46 -4
  37. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/types.d.ts +4 -4
  38. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/rig/types.d.ts.map +1 -0
  39. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/types/types.d.ts +119 -80
  40. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.d.ts.map +1 -0
  41. package/esm/dispatch.d.ts +116 -3
  42. package/esm/dispatch.d.ts.map +1 -1
  43. package/esm/dispatch.js +381 -5
  44. package/esm/elasticsearch/store.d.ts +124 -25
  45. package/esm/elasticsearch/store.d.ts.map +1 -1
  46. package/esm/elasticsearch/store.js +451 -92
  47. package/esm/entity-store.d.ts +1 -1
  48. package/esm/errors.d.ts +1 -1
  49. package/esm/errors.js +1 -1
  50. package/esm/fs/mod.d.ts +26 -0
  51. package/esm/fs/mod.d.ts.map +1 -1
  52. package/esm/fs/store.d.ts +100 -23
  53. package/esm/fs/store.d.ts.map +1 -1
  54. package/esm/fs/store.js +250 -66
  55. package/esm/glob.d.ts +114 -0
  56. package/esm/glob.d.ts.map +1 -0
  57. package/esm/glob.js +352 -0
  58. package/esm/identifiers.d.ts +18 -0
  59. package/esm/identifiers.d.ts.map +1 -0
  60. package/esm/identifiers.js +19 -0
  61. package/esm/indexeddb/fields.d.ts +32 -0
  62. package/esm/indexeddb/fields.d.ts.map +1 -0
  63. package/esm/indexeddb/fields.js +51 -0
  64. package/esm/indexeddb/store.d.ts +72 -17
  65. package/esm/indexeddb/store.d.ts.map +1 -1
  66. package/esm/indexeddb/store.js +362 -106
  67. package/esm/ipfs/mod.d.ts +19 -0
  68. package/esm/ipfs/mod.d.ts.map +1 -1
  69. package/esm/ipfs/mod.js +19 -0
  70. package/esm/ipfs/store.d.ts +82 -21
  71. package/esm/ipfs/store.d.ts.map +1 -1
  72. package/esm/ipfs/store.js +269 -68
  73. package/esm/json-fields.d.ts +61 -0
  74. package/esm/json-fields.d.ts.map +1 -0
  75. package/esm/json-fields.js +121 -0
  76. package/esm/localstorage/store.d.ts +89 -21
  77. package/esm/localstorage/store.d.ts.map +1 -1
  78. package/esm/localstorage/store.js +247 -54
  79. package/esm/memory/store.d.ts +35 -4
  80. package/esm/memory/store.d.ts.map +1 -1
  81. package/esm/memory/store.js +69 -47
  82. package/esm/mod.d.ts +2 -0
  83. package/esm/mod.d.ts.map +1 -1
  84. package/esm/mod.js +1 -0
  85. package/esm/mongo/fields.d.ts +53 -0
  86. package/esm/mongo/fields.d.ts.map +1 -0
  87. package/esm/mongo/fields.js +71 -0
  88. package/esm/mongo/mod.d.ts +19 -4
  89. package/esm/mongo/mod.d.ts.map +1 -1
  90. package/esm/mongo/mod.js +4 -2
  91. package/esm/mongo/store.d.ts +114 -25
  92. package/esm/mongo/store.d.ts.map +1 -1
  93. package/esm/mongo/store.js +355 -102
  94. package/esm/postgres/store.d.ts +50 -1
  95. package/esm/postgres/store.d.ts.map +1 -1
  96. package/esm/postgres/store.js +133 -13
  97. package/esm/read.d.ts +54 -7
  98. package/esm/read.d.ts.map +1 -1
  99. package/esm/read.js +141 -16
  100. package/esm/s3/store.d.ts +125 -23
  101. package/esm/s3/store.d.ts.map +1 -1
  102. package/esm/s3/store.js +313 -57
  103. package/esm/sqlite/columns.d.ts +33 -0
  104. package/esm/sqlite/columns.d.ts.map +1 -0
  105. package/esm/sqlite/columns.js +51 -0
  106. package/esm/sqlite/store.d.ts +73 -25
  107. package/esm/sqlite/store.d.ts.map +1 -1
  108. package/esm/sqlite/store.js +411 -107
  109. package/esm/types.d.ts +1 -1
  110. package/esm/url.d.ts +122 -19
  111. package/esm/url.d.ts.map +1 -1
  112. package/esm/url.js +218 -24
  113. package/esm/walk-via-ls.d.ts +122 -0
  114. package/esm/walk-via-ls.d.ts.map +1 -0
  115. package/esm/walk-via-ls.js +178 -0
  116. package/package.json +1 -1
  117. package/esm/byte-entity-shim.d.ts +0 -47
  118. package/esm/byte-entity-shim.d.ts.map +0 -1
  119. package/esm/byte-entity-shim.js +0 -138
  120. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/mod.d.ts.map +0 -1
  121. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/encrypt/mod.d.ts.map +0 -1
  122. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/network/policies/path-vector.d.ts.map +0 -1
  123. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/connection.d.ts.map +0 -1
  124. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/rig.d.ts.map +0 -1
  125. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/rig/types.d.ts.map +0 -1
  126. package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.d.ts.map +0 -1
  127. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/mod.js +0 -0
  128. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.d.ts +0 -0
  129. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/client-console/client.js +0 -0
  130. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.d.ts +0 -0
  131. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/encoding/encoding.js +0 -0
  132. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.d.ts +0 -0
  133. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/functional-client/functional-client.js +0 -0
  134. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.d.ts +0 -0
  135. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/match-pattern/match-pattern.js +0 -0
  136. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.d.ts +0 -0
  137. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/decorators.js +0 -0
  138. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.d.ts +0 -0
  139. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/network.js +0 -0
  140. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.d.ts +0 -0
  141. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/peer.js +0 -0
  142. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.d.ts +0 -0
  143. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/flood.js +0 -0
  144. /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
  145. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/policies/tell-and-read.js +0 -0
  146. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.d.ts +0 -0
  147. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/network/types.js +0 -0
  148. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.d.ts +0 -0
  149. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/observe-emitter/observe-emitter.js +0 -0
  150. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.d.ts +0 -0
  151. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/events.js +0 -0
  152. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.d.ts +0 -0
  153. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/hooks.js +0 -0
  154. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.d.ts +0 -0
  155. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/identity.js +0 -0
  156. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.d.ts +0 -0
  157. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/operation-handle.js +0 -0
  158. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.d.ts +0 -0
  159. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/reactions.js +0 -0
  160. /package/esm/deps/jsr.io/@bandeira-tech/b3nd-core/{0.22.0 → 0.25.0}/src/rig/types.js +0 -0
  161. /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.22.0/mod.js";
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";
@@ -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 Store.
2
+ * FsStore — Filesystem implementation of `EntityStore`.
3
3
  *
4
- * Pure mechanical byte storage with no protocol awareness. Writes one
5
- * file per entry; the file body is the payload bytes verbatim. The
6
- * store is opaque it does not parse or transform contents.
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
- * `fn=ls` / `fn=count` are shallow direct-leaves only: they list the
9
- * `.bin` files directly inside the prefix's mapped directory, never
10
- * recursing into subdirectories.
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.22.0/src/types/types.js";
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 { EntityRecord, EntitySchema } from "../entity.js";
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
- export declare class FsStore implements EntityStore<BytesOnlyEntityMeta> {
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): BytesOnlyEntityMeta;
23
- entityStatus(meta: BytesOnlyEntityMeta): Promise<"live" | "unprovisioned">;
24
- provisionEntity(_meta: BytesOnlyEntityMeta): Promise<void>;
25
- write(meta: BytesOnlyEntityMeta, entries: {
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>(meta: BytesOnlyEntityMeta, urls: string[]): Promise<Output<T>[]>;
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
- private _deleteBytes;
101
+ delete(meta: FsEntityMeta, uris: string[]): Promise<DeleteResult[]>;
41
102
  status(): Promise<StatusResult>;
42
103
  capabilities(): StoreCapabilities;
43
- private _resolvePath;
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
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/fs/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,MAAM,EACN,YAAY,EACb,MAAM,mEAAmE,CAAC;AAC3E,OAAO,EAEL,KAAK,mBAAmB,EAIzB,MAAM,wBAAwB,CAAC;AAChC,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAM/D,OAAO,KAAK,EACV,iBAAiB,EAEjB,gBAAgB,EACjB,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AA2B3C,qBAAa,OAAQ,YAAW,WAAW,CAAC,mBAAmB,CAAC;IAC9D,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,mBAAmB;IAIxD,YAAY,CAAC,IAAI,EAAE,mBAAmB,GAAG,OAAO,CAAC,MAAM,GAAG,eAAe,CAAC;IAI1E,eAAe,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI1D,KAAK,CACH,IAAI,EAAE,mBAAmB,EACzB,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,EAAE,GAC/C,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAS9B,IAAI,CAAC,CAAC,GAAG,YAAY,GAAG,SAAS,EAC/B,IAAI,EAAE,mBAAmB,EACzB,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IASvB,MAAM,CAAC,IAAI,EAAE,mBAAmB,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;YAW5D,WAAW;IAuBzB,OAAO,CAAC,UAAU;YAQJ,QAAQ;IAUtB,iFAAiF;IACjF,OAAO,CAAC,aAAa;IAQrB,4DAA4D;YAC9C,cAAc;YAgBd,GAAG;YA0BH,MAAM;YAON,YAAY;IAmBpB,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;IAyBrC,YAAY,IAAI,iBAAiB;IAIjC,OAAO,CAAC,YAAY;CAGrB"}
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 Store.
2
+ * FsStore — Filesystem implementation of `EntityStore`.
3
3
  *
4
- * Pure mechanical byte storage with no protocol awareness. Writes one
5
- * file per entry; the file body is the payload bytes verbatim. The
6
- * store is opaque it does not parse or transform contents.
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
- * `fn=ls` / `fn=count` are shallow direct-leaves only: they list the
9
- * `.bin` files directly inside the prefix's mapped directory, never
10
- * recursing into subdirectories.
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 EXT = ".bin";
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
- * Convert a relative filesystem path back to a URI.
30
- * `protocol_host/path.bin` becomes `protocol://host/path`.
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 relPathToUri(relPath) {
33
- const withoutExt = relPath.endsWith(EXT)
34
- ? relPath.slice(0, -EXT.length)
35
- : relPath;
36
- return withoutExt.replace("_", "://");
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
- // ── EntityStore surface ──────────────────────────────────────────
80
+ // ── Lifecycle ────────────────────────────────────────────────────
50
81
  entitySupport(schema) {
51
- return bytesOnlyEntitySupport(schema);
52
- }
53
- entityStatus(meta) {
54
- return Promise.resolve(meta.isBytes ? "live" : "unprovisioned");
55
- }
56
- provisionEntity(_meta) {
57
- return Promise.resolve();
58
- }
59
- write(meta, entries) {
60
- return bytesOnlyWrite(meta, STORE_NAME, entries, (e) => this._writeBytes(e));
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
- read(meta, urls) {
63
- return bytesOnlyRead(meta, STORE_NAME, urls, (u) => this._readBytes(u));
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
- delete(meta, uris) {
66
- return bytesOnlyDelete(meta, STORE_NAME, uris, (u) => this._deleteBytes(u));
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
- // ── Byte ops (BYTES_ENTITY routing) ──────────────────────────────
69
- async _writeBytes(entries) {
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 entry of entries) {
124
+ for (const { uri, record } of entries) {
72
125
  try {
73
- await this.executor.writeFile(this._resolvePath(entry.uri), entry.payload);
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", entry.uri),
138
+ ...storageFailure(err, "Write failed", uri),
80
139
  });
81
140
  }
82
141
  }
83
142
  return results;
84
143
  }
85
- _readBytes(urls) {
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
- return await this.executor.readFile(this._resolvePath(uri));
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 relDir = uriToRelPath(prefixUri).slice(0, -EXT.length).replace(/\/+$/, "");
179
+ const base = prefixUri.replace(/\/+$/, "");
115
180
  return files
116
- .filter((f) => f.endsWith(EXT) && !f.includes("/"))
117
- .map((f) => relPathToUri(`${relDir}/${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
- out.push([uri, await this._readOne(uri)]);
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
- async _deleteBytes(uris) {
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
- await this.executor.removeFile(this._resolvePath(uri));
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
- fns: ["read", "ls", "count"],
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
- _resolvePath(uri) {
192
- return `${this.rootDir}/${uriToRelPath(uri)}`;
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
  }