@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
@@ -1,17 +1,62 @@
1
1
  /**
2
- * LocalStorageStore — browser localStorage implementation of Store.
2
+ * LocalStorageStore — browser localStorage implementation of `EntityStore`.
3
3
  *
4
- * Pure mechanical byte storage with no protocol awareness. Uses
5
- * `localStorage` as a flat key→string KV. Payload bytes are
6
- * base64-encoded since `localStorage` values are strings.
4
+ * One backend, many entities. Layouts:
5
+ *
6
+ * - `BYTES_ENTITY` → key `{keyPrefix}{uri}` with a base64-encoded
7
+ * payload string. Matches the original bytes-only layout, so
8
+ * existing deployments keep working without migration.
9
+ * - any other schema → key `{keyPrefix}entities/{entityName}/{uri}`
10
+ * with a JSON-encoded record string. Per-field canonical tags
11
+ * round-trip the JSON boundary (bytes → base64, bigint → string,
12
+ * timestamp → ISO-8601 — see `./fields.ts`).
13
+ *
14
+ * ## Lifecycle
15
+ *
16
+ * `entitySupport(schema)` is pure — it plans the field set and computes
17
+ * a signature for collision detection. `provisionEntity(meta)` writes a
18
+ * tiny bookkeeping key (`{keyPrefix}__meta__/entities/{name}`) holding
19
+ * the signature; subsequent `entityStatus(meta)` reads it back and
20
+ * compares. Same-name different-shape provisioning throws.
21
+ *
22
+ * Writes/reads/deletes consume the meta directly: there is no per-call
23
+ * cache lookup gate. If `meta`'s bookkeeping is missing on the medium,
24
+ * `write` and `delete` surface a `"not provisioned"` storage failure
25
+ * per entry; `read` returns misses.
26
+ *
27
+ * ## Read params
28
+ *
29
+ * `fn=ls` / `fn=count` are shallow direct-leaves only. localStorage has
30
+ * no indexable cursor, so `_directLeaves` walks all keys once under the
31
+ * entity's `keyRoot` and standard params (`limit`/`page`/`sortBy`
32
+ * `=uri`/`sortOrder`/`format`) are applied in memory via
33
+ * `applyReadParams` from `../read.ts`.
34
+ *
35
+ * `fn=find` (v2 §3.5) walks every key under the entity's `keyRoot`
36
+ * matching the URI prefix at any depth — `_walkDeep` mirrors
37
+ * `_directLeaves` minus the `rest.includes("/")` cutoff. With
38
+ * `pushDownFind: false`, dispatch applies the embedded glob, sort,
39
+ * cursor, limit, format, and appends the cursor-as-trailing-slot.
40
+ * localStorage has no engine to leverage for push-down — iteration IS
41
+ * the work — so the symmetric shape (ls = shallow walk, find = deep
42
+ * walk; dispatch post-processes both) keeps the code minimal.
43
+ *
44
+ * `status().fns` advertises `["read", "ls", "count", "find"]`.
7
45
  */
8
- import { bytesOnlyDelete, bytesOnlyEntitySupport, bytesOnlyRead, bytesOnlyWrite, } from "../byte-entity-shim.js";
9
- import { decodeBase64, encodeBase64 } from "../deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/mod.js";
46
+ import { decodeBase64, encodeBase64 } from "../deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/mod.js";
10
47
  import { dispatchRead } from "../dispatch.js";
11
48
  import { storageFailure } from "../errors.js";
12
49
  import { toBytes } from "../payload.js";
13
50
  import { applyReadParams } from "../read.js";
51
+ import { BYTES_ENTITY, } from "../entity.js";
52
+ import { IDENTIFIER_PATTERN, isValidIdentifier } from "../identifiers.js";
53
+ import { computeSignature, decodeRecord, encodeRecord, planFields, } from "../json-fields.js";
14
54
  const STORE_NAME = "LocalStorageStore";
55
+ const ENTITY_PREFIX = "entities/";
56
+ const META_PREFIX = "__meta__/entities/";
57
+ function isBytesSchema(schema) {
58
+ return schema.name === BYTES_ENTITY.name;
59
+ }
15
60
  export class LocalStorageStore {
16
61
  keyPrefix;
17
62
  storage;
@@ -23,66 +68,145 @@ export class LocalStorageStore {
23
68
  throw new Error("localStorage is not available in this environment");
24
69
  }
25
70
  }
26
- getKey(uri) {
27
- return `${this.keyPrefix}${uri}`;
28
- }
29
- // ── EntityStore surface ──────────────────────────────────────────
71
+ // ── Lifecycle ────────────────────────────────────────────────────
30
72
  entitySupport(schema) {
31
- return bytesOnlyEntitySupport(schema);
73
+ if (isBytesSchema(schema)) {
74
+ const fields = [{ name: "payload", tag: "bytes" }];
75
+ return {
76
+ support: {
77
+ entity: schema.name,
78
+ supported: ["payload"],
79
+ unsupported: [],
80
+ },
81
+ keyRoot: this.keyPrefix,
82
+ isBytes: true,
83
+ fields,
84
+ declared: new Set(["payload"]),
85
+ bytesFields: new Set(["payload"]),
86
+ signature: computeSignature(schema.name, fields),
87
+ };
88
+ }
89
+ if (!isValidIdentifier(schema.name)) {
90
+ throw new Error(`${STORE_NAME}: entity name '${schema.name}' must match ${IDENTIFIER_PATTERN.source}`);
91
+ }
92
+ const { fields, unsupported } = planFields(schema.fields);
93
+ const declared = new Set(fields.map((f) => f.name));
94
+ const bytesFields = new Set(fields.filter((f) => f.tag === "bytes").map((f) => f.name));
95
+ return {
96
+ support: {
97
+ entity: schema.name,
98
+ supported: fields.map((f) => f.name),
99
+ unsupported,
100
+ },
101
+ keyRoot: `${this.keyPrefix}${ENTITY_PREFIX}${schema.name}/`,
102
+ isBytes: false,
103
+ fields,
104
+ declared,
105
+ bytesFields,
106
+ signature: computeSignature(schema.name, fields),
107
+ };
32
108
  }
33
109
  entityStatus(meta) {
34
- return Promise.resolve(meta.isBytes ? "live" : "unprovisioned");
35
- }
36
- provisionEntity(_meta) {
37
- return Promise.resolve();
110
+ const recorded = this._readMetaSignature(meta.support.entity);
111
+ if (recorded === null)
112
+ return Promise.resolve("unprovisioned");
113
+ return Promise.resolve(recorded === meta.signature ? "live" : "unprovisioned");
38
114
  }
39
- write(meta, entries) {
40
- return bytesOnlyWrite(meta, STORE_NAME, entries, (e) => this._writeBytes(e));
41
- }
42
- read(meta, urls) {
43
- return bytesOnlyRead(meta, STORE_NAME, urls, (u) => this._readBytes(u));
44
- }
45
- delete(meta, uris) {
46
- return bytesOnlyDelete(meta, STORE_NAME, uris, (u) => this._deleteBytes(u));
115
+ // deno-lint-ignore require-await
116
+ async provisionEntity(meta) {
117
+ const recorded = this._readMetaSignature(meta.support.entity);
118
+ if (recorded !== null) {
119
+ if (recorded !== meta.signature) {
120
+ throw new Error(`${STORE_NAME}: entity '${meta.support.entity}' is already ` +
121
+ `provisioned with a different shape`);
122
+ }
123
+ return;
124
+ }
125
+ this.storage.setItem(`${this.keyPrefix}${META_PREFIX}${meta.support.entity}`, meta.signature);
47
126
  }
48
- // ── Byte ops (BYTES_ENTITY routing) ──────────────────────────────
49
- async _writeBytes(entries) {
127
+ // ── Write ────────────────────────────────────────────────────────
128
+ async write(meta, entries) {
129
+ if (entries.length === 0)
130
+ return [];
131
+ if (this._readMetaSignature(meta.support.entity) !== meta.signature) {
132
+ return entries.map(({ uri }) => ({
133
+ success: false,
134
+ ...storageFailure(new Error(`${STORE_NAME}: entity '${meta.support.entity}' is not provisioned`), "Entity not provisioned", uri),
135
+ }));
136
+ }
50
137
  const results = [];
51
- for (const entry of entries) {
138
+ for (const { uri, record } of entries) {
139
+ const extras = Object.keys(record).filter((k) => !meta.declared.has(k));
140
+ if (extras.length > 0) {
141
+ results.push({
142
+ success: false,
143
+ ...storageFailure(new Error(`record contains keys not declared in schema '${meta.support.entity}': ${extras.join(", ")}`), "Schema mismatch", uri),
144
+ });
145
+ continue;
146
+ }
52
147
  try {
53
- const bytes = await toBytes(entry.payload);
54
- this.storage.setItem(this.getKey(entry.uri), encodeBase64(bytes));
148
+ if (meta.isBytes) {
149
+ const payload = record.payload;
150
+ if (!(payload instanceof Uint8Array) &&
151
+ !(payload instanceof ReadableStream)) {
152
+ throw new Error("BYTES_ENTITY record.payload must be Uint8Array or ReadableStream");
153
+ }
154
+ const bytes = await toBytes(payload);
155
+ this.storage.setItem(`${meta.keyRoot}${uri}`, encodeBase64(bytes));
156
+ }
157
+ else {
158
+ const normalised = await this._normaliseBytesFields(record, meta.bytesFields);
159
+ const encoded = encodeRecord(meta.fields, normalised);
160
+ this.storage.setItem(`${meta.keyRoot}${uri}`, JSON.stringify(encoded));
161
+ }
55
162
  results.push({ success: true });
56
163
  }
57
164
  catch (err) {
58
165
  results.push({
59
166
  success: false,
60
- ...storageFailure(err, "Write failed", entry.uri),
167
+ ...storageFailure(err, "Write failed", uri),
61
168
  });
62
169
  }
63
170
  }
64
171
  return results;
65
172
  }
66
- _readBytes(urls) {
173
+ // ── Read ─────────────────────────────────────────────────────────
174
+ read(meta, urls) {
67
175
  return dispatchRead(urls, STORE_NAME, {
68
- read: (p) => this._readOne(p.uri),
69
- ls: (p) => this._ls(p),
70
- count: (p) => this._count(p),
176
+ read: (p) => Promise.resolve(this._readOne(meta, p.uri)),
177
+ ls: (p) => Promise.resolve(this._ls(meta, p)),
178
+ count: (p) => Promise.resolve(this._count(meta, p)),
179
+ // v2 §3.5: find walks every key under the URI prefix (no shallow
180
+ // `/` cutoff). With `pushDownFind: false`, dispatch applies the
181
+ // glob, sort, cursor, limit, format, and appends the cursor-as-
182
+ // trailing-slot. The handler's only job is to return every
183
+ // Output under the prefix — routed through `applyReadParams` so
184
+ // `format=uris` returns `string[]` (dispatch's post-filter path
185
+ // inspects rows differently depending on format). By the time
186
+ // the handler runs, dispatch has stripped pattern, cursor,
187
+ // limit, page, and fields; `applyReadParams` ends up as a thin
188
+ // format + uri-sort pass. localStorage has no engine to leverage
189
+ // for push-down — iteration IS the work — so the symmetric shape
190
+ // (ls = shallow walk, find = deep walk; dispatch post-processes
191
+ // both) keeps the code minimal.
192
+ find: (p) => Promise.resolve(applyReadParams(this._walkDeep(meta, p.uri), p.params, STORE_NAME)),
71
193
  });
72
194
  }
73
- _readOne(uri) {
74
- const serialized = this.storage.getItem(this.getKey(uri));
75
- if (serialized === null)
195
+ _readOne(meta, uri) {
196
+ const stored = this.storage.getItem(`${meta.keyRoot}${uri}`);
197
+ if (stored === null)
76
198
  return undefined;
77
- return decodeBase64(serialized);
199
+ if (meta.isBytes)
200
+ return { payload: decodeBase64(stored) };
201
+ return decodeRecord(meta.fields, JSON.parse(stored));
78
202
  }
79
203
  /**
80
- * Walk localStorage once, returning `[uri, bytes]` for every entry
81
- * whose URI is `prefix + <segment>` with no further `/`. Subtree-only
82
- * paths are excluded by the slash check.
204
+ * Walk localStorage once, returning `Output<EntityRecord>` for every
205
+ * key under `meta.keyRoot` whose remainder is `uri + <segment>` with
206
+ * no further `/`. Subtree-only paths are excluded by the slash check.
83
207
  */
84
- _directLeaves(prefixUri) {
85
- const prefixKey = this.getKey(prefixUri);
208
+ _directLeaves(meta, uri) {
209
+ const prefixKey = `${meta.keyRoot}${uri}`;
86
210
  const out = [];
87
211
  for (let i = 0; i < this.storage.length; i++) {
88
212
  const key = this.storage.key(i);
@@ -91,25 +215,60 @@ export class LocalStorageStore {
91
215
  const rest = key.substring(prefixKey.length);
92
216
  if (rest === "" || rest.includes("/"))
93
217
  continue;
94
- const childUri = `${prefixUri}${rest}`;
95
- out.push([childUri, this._readOne(childUri)]);
218
+ const childUri = `${uri}${rest}`;
219
+ const record = this._readOne(meta, childUri);
220
+ if (record !== undefined)
221
+ out.push([childUri, record]);
96
222
  }
97
223
  return out;
98
224
  }
99
- _ls(parsed) {
100
- return applyReadParams(this._directLeaves(parsed.uri), parsed.params, STORE_NAME);
225
+ /**
226
+ * Walk localStorage once, returning `Output<EntityRecord>` for every
227
+ * key under `meta.keyRoot` whose remainder starts with `uri` — at
228
+ * any depth. Powers `fn=find`. Unlike `_directLeaves`, there is no
229
+ * `/` cutoff: keys with further slashes past the prefix are kept.
230
+ * Dispatch applies the glob (and sort/cursor/limit/format) on top.
231
+ */
232
+ _walkDeep(meta, uri) {
233
+ const prefixKey = `${meta.keyRoot}${uri}`;
234
+ const out = [];
235
+ for (let i = 0; i < this.storage.length; i++) {
236
+ const key = this.storage.key(i);
237
+ if (!key || !key.startsWith(prefixKey))
238
+ continue;
239
+ const rest = key.substring(prefixKey.length);
240
+ if (rest === "")
241
+ continue;
242
+ const childUri = `${uri}${rest}`;
243
+ const record = this._readOne(meta, childUri);
244
+ if (record !== undefined)
245
+ out.push([childUri, record]);
246
+ }
247
+ return out;
248
+ }
249
+ _ls(meta, parsed) {
250
+ return applyReadParams(this._directLeaves(meta, parsed.uri), parsed.params, STORE_NAME);
101
251
  }
102
- _count(parsed) {
252
+ _count(meta, parsed) {
103
253
  if (parsed.params.pattern !== undefined) {
104
254
  throw new Error(`${STORE_NAME}: pattern filter not supported`);
105
255
  }
106
- return this._directLeaves(parsed.uri).length;
256
+ return this._directLeaves(meta, parsed.uri).length;
107
257
  }
108
- _deleteBytes(uris) {
258
+ // ── Delete ───────────────────────────────────────────────────────
259
+ delete(meta, uris) {
260
+ if (uris.length === 0)
261
+ return Promise.resolve([]);
262
+ if (this._readMetaSignature(meta.support.entity) !== meta.signature) {
263
+ return Promise.resolve(uris.map((uri) => ({
264
+ success: false,
265
+ ...storageFailure(new Error(`${STORE_NAME}: entity '${meta.support.entity}' is not provisioned`), "Entity not provisioned", uri),
266
+ })));
267
+ }
109
268
  const results = [];
110
269
  for (const uri of uris) {
111
270
  try {
112
- this.storage.removeItem(this.getKey(uri));
271
+ this.storage.removeItem(`${meta.keyRoot}${uri}`);
113
272
  results.push({ success: true });
114
273
  }
115
274
  catch (err) {
@@ -121,27 +280,61 @@ export class LocalStorageStore {
121
280
  }
122
281
  return Promise.resolve(results);
123
282
  }
124
- // ── Status ───────────────────────────────────────────────────────
283
+ // ── Status / capabilities ────────────────────────────────────────
125
284
  status() {
126
285
  try {
127
286
  const testKey = `${this.keyPrefix}__health_check__`;
128
287
  this.storage.setItem(testKey, "ok");
129
288
  this.storage.removeItem(testKey);
289
+ const schema = [];
290
+ const metaPrefix = `${this.keyPrefix}${META_PREFIX}`;
291
+ for (let i = 0; i < this.storage.length; i++) {
292
+ const k = this.storage.key(i);
293
+ if (!k || !k.startsWith(metaPrefix))
294
+ continue;
295
+ schema.push(`entity:${k.slice(metaPrefix.length)}`);
296
+ }
130
297
  return Promise.resolve({
131
298
  status: "healthy",
132
- schema: [],
133
- fns: ["read", "ls", "count"],
299
+ schema,
300
+ fns: ["read", "ls", "count", "find"],
134
301
  });
135
302
  }
136
303
  catch {
137
304
  return Promise.resolve({
138
305
  status: "unhealthy",
139
306
  schema: [],
140
- fns: ["read", "ls", "count"],
307
+ fns: ["read", "ls", "count", "find"],
141
308
  });
142
309
  }
143
310
  }
144
311
  capabilities() {
145
312
  return { atomicBatch: false };
146
313
  }
314
+ // ── Internals ────────────────────────────────────────────────────
315
+ _readMetaSignature(entityName) {
316
+ return this.storage.getItem(`${this.keyPrefix}${META_PREFIX}${entityName}`);
317
+ }
318
+ /**
319
+ * Collect any `ReadableStream` values on `bytes`-tagged fields into
320
+ * `Uint8Array` and return a shallow-copied record. Non-stream values
321
+ * pass through; matches the normalisation pattern used by Memory and
322
+ * Postgres.
323
+ */
324
+ async _normaliseBytesFields(record, bytesFields) {
325
+ const out = { ...record };
326
+ for (const name of bytesFields) {
327
+ const v = out[name];
328
+ if (v === undefined || v === null)
329
+ continue;
330
+ if (v instanceof Uint8Array)
331
+ continue;
332
+ if (v instanceof ReadableStream) {
333
+ out[name] = await toBytes(v);
334
+ continue;
335
+ }
336
+ throw new Error(`field '${name}' must be Uint8Array or ReadableStream, got ${typeof v}`);
337
+ }
338
+ return out;
339
+ }
147
340
  }
@@ -36,8 +36,27 @@
36
36
  * record lands in the bucket. That coercion runs for every entity
37
37
  * that declares a bytes field; nothing about it is bytes-entity-
38
38
  * specific.
39
+ *
40
+ * ## Read pipeline
41
+ *
42
+ * `read()` routes through `dispatchRead` (same shell every other store
43
+ * uses) with four handlers — `read`, `ls`, `count`, `find`. The
44
+ * handlers are simple iterators over the bucket:
45
+ * - `ls` / `count`: shallow walk (direct leaves only — the
46
+ * `rest.includes("/")` cutoff in `_walkShallow`).
47
+ * - `find` (v2 §3.5): deep walk (every key under the prefix, any
48
+ * depth — `_walkRecursive`).
49
+ *
50
+ * `pushDownFind` is left **false**: memory has no engine to leverage
51
+ * for push-down, so the handler returns the raw deep walk and dispatch
52
+ * applies the glob, sort, cursor, limit, format, and appends the
53
+ * cursor-as-trailing-slot. This keeps the ls/find shape symmetric
54
+ * (shallow walk vs deep walk; dispatch post-processes both) and avoids
55
+ * duplicating `applyReadParams` plumbing inside the find handler.
56
+ *
57
+ * `status().fns` advertises `["read", "ls", "count", "find"]`.
39
58
  */
40
- import type { DeleteResult, Output, StatusResult } from "../deps/jsr.io/@bandeira-tech/b3nd-core/0.22.0/src/types/types.js";
59
+ import type { DeleteResult, Output, StatusResult } from "../deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.js";
41
60
  import type { StoreCapabilities, StoreWriteResult } from "../types.js";
42
61
  import type { EntityStore } from "../entity-store.js";
43
62
  import { type EntityMeta, type EntityRecord, type EntitySchema } from "../entity.js";
@@ -63,9 +82,21 @@ export declare class MemoryStore implements EntityStore<MemoryEntityMeta> {
63
82
  record: EntityRecord;
64
83
  }[]): Promise<StoreWriteResult[]>;
65
84
  read<T = EntityRecord | undefined>(meta: MemoryEntityMeta, urls: string[]): Promise<Output<T>[]>;
66
- private _walk;
67
- private _list;
68
- private _count;
85
+ /**
86
+ * Walk the bucket once and return every direct leaf under `uri` (the
87
+ * key has no further `/` past the prefix). Powers `ls` and `count`.
88
+ * The shallow `rest.includes("/")` cutoff is what makes this an `ls`
89
+ * walk; `_walkRecursive` is the matching deep walk for `find`.
90
+ */
91
+ private _walkShallow;
92
+ /**
93
+ * Walk the bucket once and return every record whose URI starts with
94
+ * the prefix — at any depth. Powers `fn=find`. Unlike `_walkShallow`,
95
+ * there is no `/` cutoff: keys with further slashes past the prefix
96
+ * are kept. Dispatch applies the glob (and sort/cursor/limit/format)
97
+ * on top.
98
+ */
99
+ private _walkRecursive;
69
100
  delete(meta: MemoryEntityMeta, uris: string[]): Promise<DeleteResult[]>;
70
101
  status(): Promise<StatusResult>;
71
102
  capabilities(): StoreCapabilities;
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/memory/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAsCG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,MAAM,EACN,YAAY,EACb,MAAM,mEAAmE,CAAC;AAK3E,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,YAAY,EAGlB,MAAM,cAAc,CAAC;AAKtB;;;;;;GAMG;AACH,MAAM,WAAW,gBAAiB,SAAQ,UAAU;IAClD,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAOD,qBAAa,WAAY,YAAW,WAAW,CAAC,gBAAgB,CAAC;IAC/D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAIrD,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,gBAAgB;IAwC/C,YAAY,CAChB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,MAAM,GAAG,eAAe,CAAC;IAO9B,eAAe,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBtD,KAAK,CACT,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,EAAE,GAC/C,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAsDxB,IAAI,CAAC,CAAC,GAAG,YAAY,GAAG,SAAS,EACrC,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAiBvB,OAAO,CAAC,KAAK;IAgBb,OAAO,CAAC,KAAK;IA8Bb,OAAO,CAAC,MAAM;IAYd,MAAM,CACJ,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,YAAY,EAAE,CAAC;IAgC1B,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;IAQ/B,YAAY,IAAI,iBAAiB;CAGlC"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/memory/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAyDG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,MAAM,EACN,YAAY,EACb,MAAM,mEAAmE,CAAC;AAK3E,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EACL,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,YAAY,EAGlB,MAAM,cAAc,CAAC;AAKtB;;;;;;GAMG;AACH,MAAM,WAAW,gBAAiB,SAAQ,UAAU;IAClD,QAAQ,CAAC,QAAQ,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IACvC,QAAQ,CAAC,WAAW,EAAE,WAAW,CAAC,MAAM,CAAC,CAAC;IAC1C,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAOD,qBAAa,WAAY,YAAW,WAAW,CAAC,gBAAgB,CAAC;IAC/D,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAIrD,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,gBAAgB;IAwC/C,YAAY,CAChB,IAAI,EAAE,gBAAgB,GACrB,OAAO,CAAC,MAAM,GAAG,eAAe,CAAC;IAO9B,eAAe,CAAC,IAAI,EAAE,gBAAgB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBtD,KAAK,CACT,IAAI,EAAE,gBAAgB,EACtB,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,EAAE,GAC/C,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAqD9B,IAAI,CAAC,CAAC,GAAG,YAAY,GAAG,SAAS,EAC/B,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;IAiCvB;;;;;OAKG;IACH,OAAO,CAAC,YAAY;IAgBpB;;;;;;OAMG;IACH,OAAO,CAAC,cAAc;IAoBtB,MAAM,CACJ,IAAI,EAAE,gBAAgB,EACtB,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,YAAY,EAAE,CAAC;IAgC1B,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;IAQ/B,YAAY,IAAI,iBAAiB;CAGlC"}
@@ -36,8 +36,28 @@
36
36
  * record lands in the bucket. That coercion runs for every entity
37
37
  * that declares a bytes field; nothing about it is bytes-entity-
38
38
  * specific.
39
+ *
40
+ * ## Read pipeline
41
+ *
42
+ * `read()` routes through `dispatchRead` (same shell every other store
43
+ * uses) with four handlers — `read`, `ls`, `count`, `find`. The
44
+ * handlers are simple iterators over the bucket:
45
+ * - `ls` / `count`: shallow walk (direct leaves only — the
46
+ * `rest.includes("/")` cutoff in `_walkShallow`).
47
+ * - `find` (v2 §3.5): deep walk (every key under the prefix, any
48
+ * depth — `_walkRecursive`).
49
+ *
50
+ * `pushDownFind` is left **false**: memory has no engine to leverage
51
+ * for push-down, so the handler returns the raw deep walk and dispatch
52
+ * applies the glob, sort, cursor, limit, format, and appends the
53
+ * cursor-as-trailing-slot. This keeps the ls/find shape symmetric
54
+ * (shallow walk vs deep walk; dispatch post-processes both) and avoids
55
+ * duplicating `applyReadParams` plumbing inside the find handler.
56
+ *
57
+ * `status().fns` advertises `["read", "ls", "count", "find"]`.
39
58
  */
40
- import { parseUrl } from "../url.js";
59
+ import { dispatchRead } from "../dispatch.js";
60
+ import { applyReadParams } from "../read.js";
41
61
  import { storageFailure } from "../errors.js";
42
62
  import { toBytes } from "../payload.js";
43
63
  import { TYPE_TAGS, } from "../entity.js";
@@ -142,24 +162,35 @@ export class MemoryStore {
142
162
  return results;
143
163
  }
144
164
  // ── Read ─────────────────────────────────────────────────────────
145
- // deno-lint-ignore require-await
146
- async read(meta, urls) {
165
+ read(meta, urls) {
147
166
  const bucket = this.buckets.get(meta.support.entity);
148
- return urls.map((url) => {
149
- const parsed = parseUrl(url);
150
- switch (parsed.fn) {
151
- case "read":
152
- return [url, bucket?.records.get(parsed.uri)];
153
- case "ls":
154
- return [url, this._list(bucket?.records, parsed)];
155
- case "count":
156
- return [url, this._count(bucket?.records, parsed)];
157
- default:
158
- throw new Error(`${STORE_NAME}: unsupported fn '${parsed.fn}'`);
159
- }
167
+ return dispatchRead(urls, STORE_NAME, {
168
+ read: (p) => bucket?.records.get(p.uri),
169
+ ls: (p) => applyReadParams(this._walkShallow(bucket?.records, p.uri), p.params, STORE_NAME),
170
+ count: (p) => this._walkShallow(bucket?.records, p.uri).length,
171
+ // v2 §3.5: find walks the entire bucket under the URI prefix
172
+ // (no shallow `/` cutoff). With `pushDownFind: false`, dispatch
173
+ // applies the glob, sort, cursor, limit, format, and appends the
174
+ // cursor-as-trailing-slot. The handler's only job is to return
175
+ // every Output under the prefix — routed through `applyReadParams`
176
+ // so `format=uris` returns `string[]` (dispatch's post-filter
177
+ // path inspects rows differently depending on format). By the
178
+ // time the handler runs, dispatch has stripped pattern, cursor,
179
+ // limit, page, and fields; `applyReadParams` ends up as a thin
180
+ // format + uri-sort pass. Memory has no engine to leverage for
181
+ // push-down — iteration IS the work — so the symmetric shape
182
+ // (ls = shallow walk, find = deep walk; dispatch post-processes
183
+ // both) keeps the code minimal.
184
+ find: (p) => applyReadParams(this._walkRecursive(bucket?.records, p.uri), p.params, STORE_NAME),
160
185
  });
161
186
  }
162
- _walk(bucket, uri) {
187
+ /**
188
+ * Walk the bucket once and return every direct leaf under `uri` (the
189
+ * key has no further `/` past the prefix). Powers `ls` and `count`.
190
+ * The shallow `rest.includes("/")` cutoff is what makes this an `ls`
191
+ * walk; `_walkRecursive` is the matching deep walk for `find`.
192
+ */
193
+ _walkShallow(bucket, uri) {
163
194
  if (!bucket)
164
195
  return [];
165
196
  const prefix = uri.endsWith("/") ? uri : `${uri}/`;
@@ -174,37 +205,28 @@ export class MemoryStore {
174
205
  }
175
206
  return out;
176
207
  }
177
- _list(bucket, parsed) {
178
- const { params } = parsed;
179
- if (params.pattern !== undefined) {
180
- throw new Error(`${STORE_NAME}: pattern filter not supported`);
181
- }
182
- if (params.sortBy !== undefined && params.sortBy !== "uri") {
183
- throw new Error(`${STORE_NAME}: unsupported sortBy: ${params.sortBy}`);
184
- }
185
- const format = params.format ?? "full";
186
- if (format !== "full" && format !== "uris") {
187
- throw new Error(`${STORE_NAME}: unsupported format: ${format}`);
188
- }
189
- let entries = this._walk(bucket, parsed.uri);
190
- if (params.sortBy === "uri") {
191
- const dir = params.sortOrder === "desc" ? -1 : 1;
192
- entries = [...entries].sort(([a], [b]) => a.localeCompare(b) * dir);
193
- }
194
- if (params.limit !== undefined) {
195
- const page = params.page ?? 1;
196
- const start = (page - 1) * params.limit;
197
- entries = entries.slice(start, start + params.limit);
198
- }
199
- if (format === "uris")
200
- return entries.map(([uri]) => uri);
201
- return entries;
202
- }
203
- _count(bucket, parsed) {
204
- if (parsed.params.pattern !== undefined) {
205
- throw new Error(`${STORE_NAME}: pattern filter not supported`);
208
+ /**
209
+ * Walk the bucket once and return every record whose URI starts with
210
+ * the prefix — at any depth. Powers `fn=find`. Unlike `_walkShallow`,
211
+ * there is no `/` cutoff: keys with further slashes past the prefix
212
+ * are kept. Dispatch applies the glob (and sort/cursor/limit/format)
213
+ * on top.
214
+ */
215
+ _walkRecursive(bucket, uri) {
216
+ if (!bucket)
217
+ return [];
218
+ // For find the prefix is whatever the parser produced. When the
219
+ // caller wrote `mutable://room/**` the parser gives us
220
+ // `mutable://room/` (trailing `/` from `splitLocatorGlob`); when
221
+ // they wrote `mutable://room/alice/**` we get `mutable://room/alice/`.
222
+ // Either way we include every key whose prefix matches, no further
223
+ // truncation that's what makes this the "deep" walk.
224
+ const out = [];
225
+ for (const [k, rec] of bucket) {
226
+ if (k.startsWith(uri) && k !== uri)
227
+ out.push([k, rec]);
206
228
  }
207
- return this._walk(bucket, parsed.uri).length;
229
+ return out;
208
230
  }
209
231
  // ── Delete ───────────────────────────────────────────────────────
210
232
  delete(meta, uris) {
@@ -236,7 +258,7 @@ export class MemoryStore {
236
258
  return Promise.resolve({
237
259
  status: "healthy",
238
260
  schema: [...this.buckets.keys()].map((n) => `entity:${n}`),
239
- fns: ["read", "ls", "count"],
261
+ fns: ["read", "ls", "count", "find"],
240
262
  });
241
263
  }
242
264
  capabilities() {
package/esm/mod.d.ts CHANGED
@@ -19,6 +19,8 @@
19
19
  export type { StoreCapabilities, StoreWriteResult } from "./types.js";
20
20
  export type { ParsedUrl, ReadParams } from "./url.js";
21
21
  export { buildUrl, parseUrl, uriOf } from "./url.js";
22
+ export type { WalkViaLsOptions } from "./walk-via-ls.js";
23
+ export { walkViaLs } from "./walk-via-ls.js";
22
24
  export type { EntityField, EntityMeta, EntityRecord, EntitySchema, EntitySupport, } from "./entity.js";
23
25
  export { BYTES_ENTITY, TYPE_TAGS } from "./entity.js";
24
26
  export type { EntityStore } from "./entity-store.js";
package/esm/mod.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEtE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAErD,YAAY,EACV,WAAW,EACX,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACtD,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,QAAQ,MAAM,mBAAmB,CAAC;AAC9C,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,YAAY,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAC"}
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../src/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,YAAY,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAEtE,YAAY,EAAE,SAAS,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AACtD,OAAO,EAAE,QAAQ,EAAE,QAAQ,EAAE,KAAK,EAAE,MAAM,UAAU,CAAC;AAErD,YAAY,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAE7C,YAAY,EACV,WAAW,EACX,UAAU,EACV,YAAY,EACZ,YAAY,EACZ,aAAa,GACd,MAAM,aAAa,CAAC;AACrB,OAAO,EAAE,YAAY,EAAE,SAAS,EAAE,MAAM,aAAa,CAAC;AACtD,YAAY,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAErD,OAAO,KAAK,OAAO,MAAM,kBAAkB,CAAC;AAC5C,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,QAAQ,MAAM,mBAAmB,CAAC;AAC9C,OAAO,KAAK,KAAK,MAAM,gBAAgB,CAAC;AACxC,OAAO,KAAK,MAAM,MAAM,iBAAiB,CAAC;AAC1C,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,KAAK,IAAI,MAAM,eAAe,CAAC;AACtC,OAAO,KAAK,EAAE,MAAM,aAAa,CAAC;AAClC,OAAO,KAAK,aAAa,MAAM,wBAAwB,CAAC;AACxD,OAAO,KAAK,YAAY,MAAM,uBAAuB,CAAC;AACtD,OAAO,KAAK,SAAS,MAAM,oBAAoB,CAAC"}
package/esm/mod.js CHANGED
@@ -17,6 +17,7 @@
17
17
  * `TYPE_TAGS`, …) is re-exported from the root for convenience.
18
18
  */
19
19
  export { buildUrl, parseUrl, uriOf } from "./url.js";
20
+ export { walkViaLs } from "./walk-via-ls.js";
20
21
  export { BYTES_ENTITY, TYPE_TAGS } from "./entity.js";
21
22
  export * as clients from "./clients/mod.js";
22
23
  export * as memory from "./memory/mod.js";