@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/ipfs/mod.js CHANGED
@@ -9,5 +9,24 @@
9
9
  * and streaming callers; `cat` returns a `ReadableStream<Uint8Array>`
10
10
  * to avoid materializing the block in memory unless the caller wants
11
11
  * it.
12
+ *
13
+ * ## Durability warning — session-scoped index
14
+ *
15
+ * IPFS blocks are pinned and durable, but the URI → CID index and
16
+ * provisioning bookkeeping live only in process memory (a `Map`
17
+ * inside the `IpfsStore` instance). After a process restart:
18
+ *
19
+ * - Previously-written blocks are unreachable: the store cannot map
20
+ * any URI back to its CID.
21
+ * - `entityStatus(meta)` returns `"unprovisioned"` for every entity
22
+ * until `provisionEntity` is called again.
23
+ * - `read` returns misses for all URIs that were valid before the
24
+ * restart.
25
+ *
26
+ * `IpfsStore` is effectively session-scoped as an `EntityStore`.
27
+ * Persisting the index (e.g. writing a bucket-root IPFS block on
28
+ * each write and handing the root CID to the caller for rehydration)
29
+ * is deferred — callers that need durable addressing across restarts
30
+ * should track CIDs externally or use a different backend.
12
31
  */
13
32
  export { IpfsStore } from "./store.js";
@@ -1,42 +1,103 @@
1
1
  /**
2
- * IpfsStore — IPFS implementation of Store.
2
+ * IpfsStore — IPFS implementation of `EntityStore`.
3
3
  *
4
- * Pure mechanical byte storage with no protocol awareness. Writes
5
- * pinned IPFS objects and maintains an in-memory `uri → CID` index
6
- * for lookups. The block body is the payload bytes verbatim.
4
+ * One backend, many entities. Layouts:
5
+ *
6
+ * - `BYTES_ENTITY` block holding the payload bytes verbatim. URI →
7
+ * CID is tracked in an in-memory index under the entity bucket
8
+ * `"bytes"`.
9
+ * - any other schema → block holding a JSON-encoded record. URI → CID
10
+ * is tracked in a separate in-memory index under the entity name.
11
+ * Per-field canonical `TYPE_TAGS` round-trip the JSON boundary
12
+ * (bytes → base64, bigint → string, timestamp → ISO-8601 — see
13
+ * `./fields.ts`).
14
+ *
15
+ * ## Lifecycle
16
+ *
17
+ * `entitySupport(schema)` is pure — it plans the field set and computes
18
+ * a collision signature. `provisionEntity(meta)` allocates the
19
+ * in-memory bucket and records the signature; subsequent
20
+ * `entityStatus(meta)` returns `"live"` only when the bucket's
21
+ * signature matches the meta exactly. Same-name different-shape
22
+ * provisioning throws.
23
+ *
24
+ * Note: IPFS itself has no concept of provisioning — the bucket lives
25
+ * only in memory inside this store instance. A fresh process must
26
+ * re-call `provisionEntity` before write/delete (read returns misses
27
+ * naturally), mirroring how Memory works for an in-process store.
28
+ *
29
+ * ## Read params
7
30
  *
8
31
  * `fn=ls` / `fn=count` are shallow direct-leaves only: scan the
9
- * in-memory index, keep URIs whose remainder under the prefix has no
10
- * further `/`.
32
+ * entity's in-memory URI → CID index, keep URIs whose remainder under
33
+ * the prefix has no further `/`. Standard params (`limit`/`page`/
34
+ * `sortBy=uri`/`sortOrder`/`format`) are applied in memory;
35
+ * `format=uris` and `count` skip per-leaf `cat`.
36
+ *
37
+ * `fn=find` (v2 §3.5): deep walk over the entity's in-memory URI →
38
+ * CID index (no shallow `/` cutoff). `pushDownFind` is left **false**
39
+ * — the index is just a `Map<uri, cid>`, structurally identical to
40
+ * memory's bucket; there is no IPFS-side query engine to leverage
41
+ * (CIDs are content-addressed blobs, not a queryable graph). Iteration
42
+ * IS the work, so dispatch's post-filter path (glob, sort, cursor,
43
+ * limit, format, cursor-as-trailing-slot) carries the load. Mirrors
44
+ * memory exactly (see `b3nd-save/src/memory/store.ts`).
45
+ *
46
+ * `status().fns` advertises `["read", "ls", "count", "find"]`.
11
47
  */
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";
48
+ import type { DeleteResult, Output, StatusResult } from "../deps/jsr.io/@bandeira-tech/b3nd-core/0.25.0/src/types/types.js";
14
49
  import type { EntityStore } from "../entity-store.js";
15
- import type { EntityRecord, EntitySchema } from "../entity.js";
50
+ import { type EntityMeta, type EntityRecord, type EntitySchema } from "../entity.js";
16
51
  import type { StoreCapabilities, StoreWriteResult } from "../types.js";
52
+ import { type FieldPlan } from "../json-fields.js";
17
53
  import type { IpfsExecutor } from "./mod.js";
18
- export declare class IpfsStore implements EntityStore<BytesOnlyEntityMeta> {
54
+ /**
55
+ * IpfsStore-specific entity handle.
56
+ *
57
+ * `isBytes` selects the raw-payload block layout vs the JSON-encoded
58
+ * record block layout. `fields`/`declared`/`bytesFields` drive
59
+ * write-time validation and encoding; `signature` is used for
60
+ * collision detection in `entityStatus` / `provisionEntity`.
61
+ */
62
+ export interface IpfsEntityMeta extends EntityMeta {
63
+ readonly isBytes: boolean;
64
+ readonly fields: readonly FieldPlan[];
65
+ readonly declared: ReadonlySet<string>;
66
+ readonly bytesFields: ReadonlySet<string>;
67
+ readonly signature: string;
68
+ }
69
+ export declare class IpfsStore implements EntityStore<IpfsEntityMeta> {
19
70
  private readonly executor;
20
- private readonly index;
71
+ private readonly buckets;
21
72
  constructor(executor: IpfsExecutor);
22
- entitySupport(schema: EntitySchema): BytesOnlyEntityMeta;
23
- entityStatus(meta: BytesOnlyEntityMeta): Promise<"live" | "unprovisioned">;
24
- provisionEntity(_meta: BytesOnlyEntityMeta): Promise<void>;
25
- write(meta: BytesOnlyEntityMeta, entries: {
73
+ entitySupport(schema: EntitySchema): IpfsEntityMeta;
74
+ entityStatus(meta: IpfsEntityMeta): Promise<"live" | "unprovisioned">;
75
+ provisionEntity(meta: IpfsEntityMeta): Promise<void>;
76
+ write(meta: IpfsEntityMeta, entries: {
26
77
  uri: string;
27
78
  record: EntityRecord;
28
79
  }[]): 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;
80
+ read<T = EntityRecord | undefined>(meta: IpfsEntityMeta, urls: string[]): Promise<Output<T>[]>;
33
81
  private _readOne;
34
- /** Shallow direct-leaves from the in-memory URI index. */
82
+ /** Shallow direct-leaves from the entity's in-memory URI index. */
35
83
  private _directLeafUris;
36
84
  private _ls;
37
85
  private _count;
38
- private _deleteBytes;
86
+ /**
87
+ * Deep walk: every URI under the prefix at any depth. Powers
88
+ * `fn=find`. Unlike `_directLeafUris`, there is no `/` cutoff — keys
89
+ * with further slashes past the prefix are kept. Dispatch applies the
90
+ * glob (and sort/cursor/limit/format) on top.
91
+ */
92
+ private _allUris;
93
+ private _find;
94
+ delete(meta: IpfsEntityMeta, uris: string[]): Promise<DeleteResult[]>;
39
95
  status(): Promise<StatusResult>;
40
96
  capabilities(): StoreCapabilities;
97
+ /**
98
+ * Collect any `ReadableStream` values on `bytes`-tagged fields into
99
+ * `Uint8Array` and return a shallow-copied record.
100
+ */
101
+ private _normaliseBytesFields;
41
102
  }
42
103
  //# sourceMappingURL=store.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/ipfs/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,YAAY,EAAE,MAAM,UAAU,CAAC;AAQ7C,qBAAa,SAAU,YAAW,WAAW,CAAC,mBAAmB,CAAC;IAChE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAiC;gBAE3C,QAAQ,EAAE,YAAY;IAOlC,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;IAiCzB,OAAO,CAAC,UAAU;YAQJ,QAAQ;IAYtB,0DAA0D;IAC1D,OAAO,CAAC,eAAe;YAWT,GAAG;IA0BjB,OAAO,CAAC,MAAM;YAOA,YAAY;IA4BpB,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;IAqCrC,YAAY,IAAI,iBAAiB;CAGlC"}
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../src/ipfs/store.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8CG;AAEH,OAAO,KAAK,EACV,YAAY,EACZ,MAAM,EACN,YAAY,EACb,MAAM,mEAAmE,CAAC;AAM3E,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACtD,OAAO,EAEL,KAAK,UAAU,EACf,KAAK,YAAY,EACjB,KAAK,YAAY,EAClB,MAAM,cAAc,CAAC;AAEtB,OAAO,KAAK,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACvE,OAAO,EAIL,KAAK,SAAS,EAEf,MAAM,mBAAmB,CAAC;AAC3B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAa7C;;;;;;;GAOG;AACH,MAAM,WAAW,cAAe,SAAQ,UAAU;IAChD,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,MAAM,EAAE,SAAS,SAAS,EAAE,CAAC;IACtC,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;AAMD,qBAAa,SAAU,YAAW,WAAW,CAAC,cAAc,CAAC;IAC3D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAe;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAmC;gBAE/C,QAAQ,EAAE,YAAY;IAOlC,aAAa,CAAC,MAAM,EAAE,YAAY,GAAG,cAAc;IAyC7C,YAAY,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,MAAM,GAAG,eAAe,CAAC;IAOrE,eAAe,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBpD,KAAK,CACT,IAAI,EAAE,cAAc,EACpB,OAAO,EAAE;QAAE,GAAG,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,YAAY,CAAA;KAAE,EAAE,GAC/C,OAAO,CAAC,gBAAgB,EAAE,CAAC;IA+E9B,IAAI,CAAC,CAAC,GAAG,YAAY,GAAG,SAAS,EAC/B,IAAI,EAAE,cAAc,EACpB,IAAI,EAAE,MAAM,EAAE,GACb,OAAO,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC;YAmBT,QAAQ;IAmBtB,mEAAmE;IACnE,OAAO,CAAC,eAAe;YAaT,GAAG;IA4BjB,OAAO,CAAC,MAAM;IAOd;;;;;OAKG;IACH,OAAO,CAAC,QAAQ;YAUF,KAAK;IA6Bb,MAAM,CAAC,IAAI,EAAE,cAAc,EAAE,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC;IAwCrE,MAAM,IAAI,OAAO,CAAC,YAAY,CAAC;IA8BrC,YAAY,IAAI,iBAAiB;IAMjC;;;OAGG;YACW,qBAAqB;CAmBpC"}
package/esm/ipfs/store.js CHANGED
@@ -1,97 +1,231 @@
1
1
  /**
2
- * IpfsStore — IPFS implementation of Store.
2
+ * IpfsStore — IPFS implementation of `EntityStore`.
3
3
  *
4
- * Pure mechanical byte storage with no protocol awareness. Writes
5
- * pinned IPFS objects and maintains an in-memory `uri → CID` index
6
- * for lookups. The block body is the payload bytes verbatim.
4
+ * One backend, many entities. Layouts:
5
+ *
6
+ * - `BYTES_ENTITY` block holding the payload bytes verbatim. URI →
7
+ * CID is tracked in an in-memory index under the entity bucket
8
+ * `"bytes"`.
9
+ * - any other schema → block holding a JSON-encoded record. URI → CID
10
+ * is tracked in a separate in-memory index under the entity name.
11
+ * Per-field canonical `TYPE_TAGS` round-trip the JSON boundary
12
+ * (bytes → base64, bigint → string, timestamp → ISO-8601 — see
13
+ * `./fields.ts`).
14
+ *
15
+ * ## Lifecycle
16
+ *
17
+ * `entitySupport(schema)` is pure — it plans the field set and computes
18
+ * a collision signature. `provisionEntity(meta)` allocates the
19
+ * in-memory bucket and records the signature; subsequent
20
+ * `entityStatus(meta)` returns `"live"` only when the bucket's
21
+ * signature matches the meta exactly. Same-name different-shape
22
+ * provisioning throws.
23
+ *
24
+ * Note: IPFS itself has no concept of provisioning — the bucket lives
25
+ * only in memory inside this store instance. A fresh process must
26
+ * re-call `provisionEntity` before write/delete (read returns misses
27
+ * naturally), mirroring how Memory works for an in-process store.
28
+ *
29
+ * ## Read params
7
30
  *
8
31
  * `fn=ls` / `fn=count` are shallow direct-leaves only: scan the
9
- * in-memory index, keep URIs whose remainder under the prefix has no
10
- * further `/`.
32
+ * entity's in-memory URI → CID index, keep URIs whose remainder under
33
+ * the prefix has no further `/`. Standard params (`limit`/`page`/
34
+ * `sortBy=uri`/`sortOrder`/`format`) are applied in memory;
35
+ * `format=uris` and `count` skip per-leaf `cat`.
36
+ *
37
+ * `fn=find` (v2 §3.5): deep walk over the entity's in-memory URI →
38
+ * CID index (no shallow `/` cutoff). `pushDownFind` is left **false**
39
+ * — the index is just a `Map<uri, cid>`, structurally identical to
40
+ * memory's bucket; there is no IPFS-side query engine to leverage
41
+ * (CIDs are content-addressed blobs, not a queryable graph). Iteration
42
+ * IS the work, so dispatch's post-filter path (glob, sort, cursor,
43
+ * limit, format, cursor-as-trailing-slot) carries the load. Mirrors
44
+ * memory exactly (see `b3nd-save/src/memory/store.ts`).
45
+ *
46
+ * `status().fns` advertises `["read", "ls", "count", "find"]`.
11
47
  */
12
- import { bytesOnlyDelete, bytesOnlyEntitySupport, bytesOnlyRead, bytesOnlyWrite, } from "../byte-entity-shim.js";
13
48
  import { dispatchRead } from "../dispatch.js";
14
49
  import { storageFailure } from "../errors.js";
15
- import { validateReadParams } from "../read.js";
50
+ import { toBytes } from "../payload.js";
51
+ import { applyReadParams, validateReadParams } from "../read.js";
52
+ import { BYTES_ENTITY, } from "../entity.js";
53
+ import { IDENTIFIER_PATTERN, isValidIdentifier } from "../identifiers.js";
54
+ import { computeSignature, decodeRecord, encodeRecord, planFields, } from "../json-fields.js";
16
55
  const STORE_NAME = "IpfsStore";
56
+ function isBytesSchema(schema) {
57
+ return schema.name === BYTES_ENTITY.name;
58
+ }
17
59
  export class IpfsStore {
18
60
  executor;
19
- index = new Map();
61
+ buckets = new Map();
20
62
  constructor(executor) {
21
63
  if (!executor)
22
64
  throw new Error("executor is required");
23
65
  this.executor = executor;
24
66
  }
25
- // ── EntityStore surface ──────────────────────────────────────────
67
+ // ── Lifecycle ────────────────────────────────────────────────────
26
68
  entitySupport(schema) {
27
- return bytesOnlyEntitySupport(schema);
28
- }
29
- entityStatus(meta) {
30
- return Promise.resolve(meta.isBytes ? "live" : "unprovisioned");
31
- }
32
- provisionEntity(_meta) {
33
- return Promise.resolve();
34
- }
35
- write(meta, entries) {
36
- return bytesOnlyWrite(meta, STORE_NAME, entries, (e) => this._writeBytes(e));
69
+ if (isBytesSchema(schema)) {
70
+ const fields = [{ name: "payload", tag: "bytes" }];
71
+ return {
72
+ support: {
73
+ entity: schema.name,
74
+ supported: ["payload"],
75
+ unsupported: [],
76
+ },
77
+ isBytes: true,
78
+ fields,
79
+ declared: new Set(["payload"]),
80
+ bytesFields: new Set(["payload"]),
81
+ signature: computeSignature(schema.name, fields),
82
+ };
83
+ }
84
+ if (!isValidIdentifier(schema.name)) {
85
+ throw new Error(`${STORE_NAME}: entity name '${schema.name}' must match ${IDENTIFIER_PATTERN.source}`);
86
+ }
87
+ const { fields, unsupported } = planFields(schema.fields);
88
+ const declared = new Set(fields.map((f) => f.name));
89
+ const bytesFields = new Set(fields.filter((f) => f.tag === "bytes").map((f) => f.name));
90
+ return {
91
+ support: {
92
+ entity: schema.name,
93
+ supported: fields.map((f) => f.name),
94
+ unsupported,
95
+ },
96
+ isBytes: false,
97
+ fields,
98
+ declared,
99
+ bytesFields,
100
+ signature: computeSignature(schema.name, fields),
101
+ };
37
102
  }
38
- read(meta, urls) {
39
- return bytesOnlyRead(meta, STORE_NAME, urls, (u) => this._readBytes(u));
103
+ // deno-lint-ignore require-await
104
+ async entityStatus(meta) {
105
+ const bucket = this.buckets.get(meta.support.entity);
106
+ if (!bucket)
107
+ return "unprovisioned";
108
+ return bucket.signature === meta.signature ? "live" : "unprovisioned";
40
109
  }
41
- delete(meta, uris) {
42
- return bytesOnlyDelete(meta, STORE_NAME, uris, (u) => this._deleteBytes(u));
110
+ // deno-lint-ignore require-await
111
+ async provisionEntity(meta) {
112
+ const existing = this.buckets.get(meta.support.entity);
113
+ if (existing) {
114
+ if (existing.signature !== meta.signature) {
115
+ throw new Error(`${STORE_NAME}: entity '${meta.support.entity}' is already ` +
116
+ `provisioned with a different shape`);
117
+ }
118
+ return;
119
+ }
120
+ this.buckets.set(meta.support.entity, {
121
+ signature: meta.signature,
122
+ index: new Map(),
123
+ });
43
124
  }
44
- // ── Byte ops (BYTES_ENTITY routing) ──────────────────────────────
45
- async _writeBytes(entries) {
125
+ // ── Write ────────────────────────────────────────────────────────
126
+ async write(meta, entries) {
127
+ if (entries.length === 0)
128
+ return [];
129
+ const bucket = this.buckets.get(meta.support.entity);
130
+ if (!bucket || bucket.signature !== meta.signature) {
131
+ return entries.map(({ uri }) => ({
132
+ success: false,
133
+ ...storageFailure(new Error(`${STORE_NAME}: entity '${meta.support.entity}' is not provisioned`), "Entity not provisioned", uri),
134
+ }));
135
+ }
46
136
  const results = [];
47
- for (const entry of entries) {
137
+ for (const { uri, record } of entries) {
138
+ const extras = Object.keys(record).filter((k) => !meta.declared.has(k));
139
+ if (extras.length > 0) {
140
+ results.push({
141
+ success: false,
142
+ ...storageFailure(new Error(`record contains keys not declared in schema '${meta.support.entity}': ${extras.join(", ")}`), "Schema mismatch", uri),
143
+ });
144
+ continue;
145
+ }
48
146
  try {
49
- const cid = await this.executor.add(entry.payload);
147
+ let body;
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
+ body = payload;
155
+ }
156
+ else {
157
+ const normalised = await this._normaliseBytesFields(record, meta.bytesFields);
158
+ const json = JSON.stringify(encodeRecord(meta.fields, normalised));
159
+ body = new TextEncoder().encode(json);
160
+ }
161
+ const cid = await this.executor.add(body);
50
162
  await this.executor.pin(cid);
51
- // Unpin the old CID if this URI was already indexed
52
- const existing = this.index.get(entry.uri);
163
+ const existing = bucket.index.get(uri);
53
164
  if (existing) {
54
165
  try {
55
166
  await this.executor.unpin(existing.cid);
56
167
  }
57
168
  catch {
58
- // Old CID may already be unpinned — ignore
169
+ // Old CID may already be unpinned — ignore.
59
170
  }
60
171
  }
61
- this.index.set(entry.uri, { cid });
172
+ bucket.index.set(uri, { cid });
62
173
  results.push({ success: true });
63
174
  }
64
175
  catch (err) {
65
176
  results.push({
66
177
  success: false,
67
- ...storageFailure(err, "Write failed", entry.uri),
178
+ ...storageFailure(err, "Write failed", uri),
68
179
  });
69
180
  }
70
181
  }
71
182
  return results;
72
183
  }
73
- _readBytes(urls) {
184
+ // ── Read ─────────────────────────────────────────────────────────
185
+ read(meta, urls) {
74
186
  return dispatchRead(urls, STORE_NAME, {
75
- read: (p) => this._readOne(p.uri),
76
- ls: (p) => this._ls(p),
77
- count: (p) => this._count(p),
187
+ read: (p) => this._readOne(meta, p.uri),
188
+ ls: (p) => this._ls(meta, p),
189
+ count: (p) => Promise.resolve(this._count(meta, p)),
190
+ // v2 §3.5: find walks the entire in-memory CID index under the
191
+ // URI prefix (no shallow `/` cutoff). With `pushDownFind: false`,
192
+ // dispatch applies the glob, sort, cursor, limit, format, and
193
+ // appends the cursor-as-trailing-slot. By the time the handler
194
+ // runs, dispatch has stripped pattern, cursor, limit, page, and
195
+ // fields; `applyReadParams` ends up as a thin format + uri-sort
196
+ // pass. IPFS has no queryable engine — CIDs are content-addressed
197
+ // blobs — so iteration IS the work; the symmetric shape with
198
+ // memory (ls = shallow walk, find = deep walk; dispatch
199
+ // post-processes both) keeps the code minimal.
200
+ find: (p) => this._find(meta, p),
78
201
  });
79
202
  }
80
- async _readOne(uri) {
81
- const entry = this.index.get(uri);
203
+ async _readOne(meta, uri) {
204
+ const bucket = this.buckets.get(meta.support.entity);
205
+ if (!bucket)
206
+ return undefined;
207
+ const entry = bucket.index.get(uri);
82
208
  if (!entry)
83
209
  return undefined;
84
210
  try {
85
- return await this.executor.cat(entry.cid);
211
+ const stream = await this.executor.cat(entry.cid);
212
+ if (meta.isBytes)
213
+ return { payload: stream };
214
+ const bytes = await new Response(stream).bytes();
215
+ const text = new TextDecoder().decode(bytes);
216
+ return decodeRecord(meta.fields, JSON.parse(text));
86
217
  }
87
218
  catch {
88
219
  return undefined;
89
220
  }
90
221
  }
91
- /** Shallow direct-leaves from the in-memory URI index. */
92
- _directLeafUris(prefixUri) {
222
+ /** Shallow direct-leaves from the entity's in-memory URI index. */
223
+ _directLeafUris(meta, prefixUri) {
224
+ const bucket = this.buckets.get(meta.support.entity);
225
+ if (!bucket)
226
+ return [];
93
227
  const out = [];
94
- for (const uri of this.index.keys()) {
228
+ for (const uri of bucket.index.keys()) {
95
229
  if (!uri.startsWith(prefixUri))
96
230
  continue;
97
231
  const tail = uri.slice(prefixUri.length);
@@ -101,11 +235,11 @@ export class IpfsStore {
101
235
  }
102
236
  return out;
103
237
  }
104
- async _ls(parsed) {
238
+ async _ls(meta, parsed) {
105
239
  validateReadParams(parsed.params, STORE_NAME);
106
240
  const { params } = parsed;
107
241
  const format = params.format ?? "full";
108
- let uris = this._directLeafUris(parsed.uri);
242
+ let uris = this._directLeafUris(meta, parsed.uri);
109
243
  if (params.sortBy === "uri") {
110
244
  const dir = params.sortOrder === "desc" ? -1 : 1;
111
245
  uris = [...uris].sort((a, b) => a.localeCompare(b) * dir);
@@ -119,29 +253,82 @@ export class IpfsStore {
119
253
  return uris;
120
254
  const out = [];
121
255
  for (const uri of uris) {
122
- out.push([uri, await this._readOne(uri)]);
256
+ const rec = await this._readOne(meta, uri);
257
+ if (rec !== undefined)
258
+ out.push([uri, rec]);
123
259
  }
124
260
  return out;
125
261
  }
126
- _count(parsed) {
262
+ _count(meta, parsed) {
127
263
  if (parsed.params.pattern !== undefined) {
128
264
  throw new Error(`${STORE_NAME}: pattern filter not supported`);
129
265
  }
130
- return this._directLeafUris(parsed.uri).length;
266
+ return this._directLeafUris(meta, parsed.uri).length;
267
+ }
268
+ /**
269
+ * Deep walk: every URI under the prefix at any depth. Powers
270
+ * `fn=find`. Unlike `_directLeafUris`, there is no `/` cutoff — keys
271
+ * with further slashes past the prefix are kept. Dispatch applies the
272
+ * glob (and sort/cursor/limit/format) on top.
273
+ */
274
+ _allUris(meta, prefixUri) {
275
+ const bucket = this.buckets.get(meta.support.entity);
276
+ if (!bucket)
277
+ return [];
278
+ const out = [];
279
+ for (const uri of bucket.index.keys()) {
280
+ if (uri.startsWith(prefixUri) && uri !== prefixUri)
281
+ out.push(uri);
282
+ }
283
+ return out;
284
+ }
285
+ async _find(meta, parsed) {
286
+ validateReadParams(parsed.params, STORE_NAME);
287
+ const uris = this._allUris(meta, parsed.uri);
288
+ // Collect the rows once; let `applyReadParams` honour format /
289
+ // sort / cursor / limit. `format=uris` skips per-leaf `cat` since
290
+ // the bucket already holds the URIs. Dispatch has stripped the
291
+ // glob + cursor + limit by the time we run (pushDownFind: false),
292
+ // so this path is effectively a thin format/sort pass.
293
+ const format = parsed.params.format ?? "full";
294
+ if (format === "uris") {
295
+ // Build placeholder rows so applyReadParams's sort/cursor/limit
296
+ // can operate, then it projects to `string[]`. We never need the
297
+ // record values on this branch.
298
+ const rows = uris.map((u) => [u, {}]);
299
+ return applyReadParams(rows, parsed.params, STORE_NAME);
300
+ }
301
+ const rows = [];
302
+ for (const uri of uris) {
303
+ const rec = await this._readOne(meta, uri);
304
+ if (rec !== undefined)
305
+ rows.push([uri, rec]);
306
+ }
307
+ return applyReadParams(rows, parsed.params, STORE_NAME);
131
308
  }
132
- async _deleteBytes(uris) {
309
+ // ── Delete ───────────────────────────────────────────────────────
310
+ async delete(meta, uris) {
311
+ if (uris.length === 0)
312
+ return [];
313
+ const bucket = this.buckets.get(meta.support.entity);
314
+ if (!bucket || bucket.signature !== meta.signature) {
315
+ return uris.map((uri) => ({
316
+ success: false,
317
+ ...storageFailure(new Error(`${STORE_NAME}: entity '${meta.support.entity}' is not provisioned`), "Entity not provisioned", uri),
318
+ }));
319
+ }
133
320
  const results = [];
134
321
  for (const uri of uris) {
135
322
  try {
136
- const entry = this.index.get(uri);
323
+ const entry = bucket.index.get(uri);
137
324
  if (entry) {
138
325
  try {
139
326
  await this.executor.unpin(entry.cid);
140
327
  }
141
328
  catch {
142
- // CID may already be unpinned — ignore
329
+ // CID may already be unpinned — ignore.
143
330
  }
144
- this.index.delete(uri);
331
+ bucket.index.delete(uri);
145
332
  }
146
333
  results.push({ success: true });
147
334
  }
@@ -154,7 +341,7 @@ export class IpfsStore {
154
341
  }
155
342
  return results;
156
343
  }
157
- // ── Status ───────────────────────────────────────────────────────
344
+ // ── Status / capabilities ────────────────────────────────────────
158
345
  async status() {
159
346
  try {
160
347
  const online = await this.executor.isOnline();
@@ -162,36 +349,50 @@ export class IpfsStore {
162
349
  return {
163
350
  status: "unhealthy",
164
351
  message: "IPFS node is not reachable",
165
- fns: ["read", "ls", "count"],
352
+ fns: ["read", "ls", "count", "find"],
166
353
  };
167
354
  }
168
- // Derive schema from indexed URIs
169
- const programs = new Set();
170
- for (const uri of this.index.keys()) {
171
- try {
172
- const url = new URL(uri);
173
- programs.add(`${url.protocol}//${url.hostname}`);
174
- }
175
- catch {
176
- // skip malformed URIs
177
- }
355
+ let indexedUris = 0;
356
+ for (const bucket of this.buckets.values()) {
357
+ indexedUris += bucket.index.size;
178
358
  }
179
359
  return {
180
360
  status: "healthy",
181
- schema: [...programs],
182
- fns: ["read", "ls", "count"],
183
- details: { indexedUris: this.index.size },
361
+ schema: [...this.buckets.keys()].map((n) => `entity:${n}`),
362
+ fns: ["read", "ls", "count", "find"],
363
+ details: { indexedUris },
184
364
  };
185
365
  }
186
366
  catch (error) {
187
367
  return {
188
368
  status: "unhealthy",
189
369
  message: error instanceof Error ? error.message : String(error),
190
- fns: ["read", "ls", "count"],
370
+ fns: ["read", "ls", "count", "find"],
191
371
  };
192
372
  }
193
373
  }
194
374
  capabilities() {
195
375
  return { atomicBatch: false };
196
376
  }
377
+ // ── Internals ────────────────────────────────────────────────────
378
+ /**
379
+ * Collect any `ReadableStream` values on `bytes`-tagged fields into
380
+ * `Uint8Array` and return a shallow-copied record.
381
+ */
382
+ async _normaliseBytesFields(record, bytesFields) {
383
+ const out = { ...record };
384
+ for (const name of bytesFields) {
385
+ const v = out[name];
386
+ if (v === undefined || v === null)
387
+ continue;
388
+ if (v instanceof Uint8Array)
389
+ continue;
390
+ if (v instanceof ReadableStream) {
391
+ out[name] = await toBytes(v);
392
+ continue;
393
+ }
394
+ throw new Error(`field '${name}' must be Uint8Array or ReadableStream, got ${typeof v}`);
395
+ }
396
+ return out;
397
+ }
197
398
  }