@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,24 +1,64 @@
1
1
  /**
2
- * IndexedDBStore — browser IndexedDB implementation of Store.
2
+ * IndexedDBStore — browser IndexedDB implementation of `EntityStore`.
3
3
  *
4
- * Pure mechanical byte storage with no protocol awareness. Uses
5
- * IndexedDB for large-scale persistent browser storage. IndexedDB's
6
- * structured clone preserves `Uint8Array` natively.
4
+ * Two layouts share one object store, separated by storage-key prefix.
5
+ * IDB's structured clone preserves typed values natively
6
+ * (`Uint8Array`, `Date`, `BigInt`, plain JSON), so records are stored
7
+ * verbatim — no JSON encoding step on the way in or out.
7
8
  *
8
- * `fn=ls` / `fn=count` are shallow direct-leaves only: cursor over
9
- * the `uri_index`, keep records whose URI is `prefix + <segment>`
10
- * with no further `/`. The `format=uris` fast path and `count` only
11
- * touch the index key — they never load the full record's payload.
9
+ * - `BYTES_ENTITY` record `{ uri, payload }` keyed by `uri` (the
10
+ * original byte-store layout existing deployments keep working).
11
+ * - any other schema record `{ uri, record }` keyed by
12
+ * `__entities__/{entityName}/{originalUri}`. The `__entities__/`
13
+ * prefix isolates entity slots from byte slots in the cursor's
14
+ * ordered key space.
15
+ *
16
+ * ## Lifecycle
17
+ *
18
+ * `entitySupport(schema)` is pure — it plans the field set and computes
19
+ * a collision signature. `provisionEntity(meta)` writes a tiny meta
20
+ * record (`__meta__/{name}`) holding the signature; subsequent
21
+ * `entityStatus(meta)` reads it back. Same-name different-shape
22
+ * provisioning throws.
23
+ *
24
+ * IDB needs an `onupgradeneeded` to create object stores, but since we
25
+ * use a single store for everything, schema migrations are not required
26
+ * to grow new entities.
27
+ *
28
+ * ## Read params
29
+ *
30
+ * `fn=ls` / `fn=count` are shallow direct-leaves only: cursor over the
31
+ * `uri_index` bounded by `[storageKey, storageKey + ￿)`, keep records
32
+ * whose remainder under the prefix has no further `/`. Standard params
33
+ * (`limit`/`page`/`sortBy=uri`/`sortOrder`/`format=uris`) are honoured
34
+ * via cursor walking — `format=uris` and `count` skip loading payloads
35
+ * via `openKeyCursor`.
36
+ *
37
+ * `fn=find` (v2 §3.5) is the deep counterpart: same cursor, same
38
+ * range, no `rest.includes("/")` tail cutoff. With `pushDownFind:
39
+ * false` the handler returns every descendant under the prefix and
40
+ * dispatch applies the glob, sort, cursor, limit, format, and appends
41
+ * the cursor-as-trailing-slot. IDB has no engine for glob/regex
42
+ * push-down — its cursor only honours key-range and direction — so
43
+ * dispatch's post-filter path is the honest fit (same shape as memory
44
+ * PR #84).
12
45
  */
13
46
  import * as dntShim from "../_dnt.shims.js";
14
- import { bytesOnlyDelete, bytesOnlyEntitySupport, bytesOnlyRead, bytesOnlyWrite, } from "../byte-entity-shim.js";
15
47
  import { dispatchRead } from "../dispatch.js";
16
48
  import { storageFailure } from "../errors.js";
17
49
  import { toBytes } from "../payload.js";
18
50
  import { validateReadParams } from "../read.js";
51
+ import { BYTES_ENTITY, } from "../entity.js";
52
+ import { IDENTIFIER_PATTERN, isValidIdentifier } from "../identifiers.js";
53
+ import { computeSignature, planFields } from "./fields.js";
19
54
  const STORE_NAME = "IndexedDBStore";
55
+ const ENTITY_PREFIX = "__entities__/";
56
+ const META_PREFIX = "__meta__/";
20
57
  // deno-lint-ignore no-explicit-any
21
58
  const idbKeyRange = dntShim.dntGlobalThis.IDBKeyRange;
59
+ function isBytesSchema(schema) {
60
+ return schema.name === BYTES_ENTITY.name;
61
+ }
22
62
  export class IndexedDBStore {
23
63
  databaseName;
24
64
  storeName;
@@ -63,93 +103,185 @@ export class IndexedDBStore {
63
103
  const db = await this.initDB();
64
104
  return db.transaction([this.storeName], mode).objectStore(this.storeName);
65
105
  }
66
- // ── EntityStore surface ──────────────────────────────────────────
106
+ // ── Lifecycle ────────────────────────────────────────────────────
67
107
  entitySupport(schema) {
68
- return bytesOnlyEntitySupport(schema);
69
- }
70
- entityStatus(meta) {
71
- return Promise.resolve(meta.isBytes ? "live" : "unprovisioned");
72
- }
73
- provisionEntity(_meta) {
74
- return Promise.resolve();
75
- }
76
- write(meta, entries) {
77
- return bytesOnlyWrite(meta, STORE_NAME, entries, (e) => this._writeBytes(e));
108
+ if (isBytesSchema(schema)) {
109
+ const fields = [{ name: "payload", tag: "bytes" }];
110
+ return {
111
+ support: {
112
+ entity: schema.name,
113
+ supported: ["payload"],
114
+ unsupported: [],
115
+ },
116
+ storageRoot: "",
117
+ isBytes: true,
118
+ fields,
119
+ declared: new Set(["payload"]),
120
+ bytesFields: new Set(["payload"]),
121
+ signature: computeSignature(schema.name, fields),
122
+ };
123
+ }
124
+ if (!isValidIdentifier(schema.name)) {
125
+ throw new Error(`${STORE_NAME}: entity name '${schema.name}' must match ${IDENTIFIER_PATTERN.source}`);
126
+ }
127
+ const { fields, unsupported } = planFields(schema.fields);
128
+ const declared = new Set(fields.map((f) => f.name));
129
+ const bytesFields = new Set(fields.filter((f) => f.tag === "bytes").map((f) => f.name));
130
+ return {
131
+ support: {
132
+ entity: schema.name,
133
+ supported: fields.map((f) => f.name),
134
+ unsupported,
135
+ },
136
+ storageRoot: `${ENTITY_PREFIX}${schema.name}/`,
137
+ isBytes: false,
138
+ fields,
139
+ declared,
140
+ bytesFields,
141
+ signature: computeSignature(schema.name, fields),
142
+ };
78
143
  }
79
- read(meta, urls) {
80
- return bytesOnlyRead(meta, STORE_NAME, urls, (u) => this._readBytes(u));
144
+ async entityStatus(meta) {
145
+ const recorded = await this._readMetaSignature(meta.support.entity);
146
+ if (recorded === null)
147
+ return "unprovisioned";
148
+ return recorded === meta.signature ? "live" : "unprovisioned";
81
149
  }
82
- delete(meta, uris) {
83
- return bytesOnlyDelete(meta, STORE_NAME, uris, (u) => this._deleteBytes(u));
150
+ async provisionEntity(meta) {
151
+ const recorded = await this._readMetaSignature(meta.support.entity);
152
+ if (recorded !== null) {
153
+ if (recorded !== meta.signature) {
154
+ throw new Error(`${STORE_NAME}: entity '${meta.support.entity}' is already ` +
155
+ `provisioned with a different shape`);
156
+ }
157
+ return;
158
+ }
159
+ const doc = {
160
+ uri: `${META_PREFIX}${meta.support.entity}`,
161
+ signature: meta.signature,
162
+ };
163
+ const store = await this.getStore("readwrite");
164
+ await new Promise((resolve, reject) => {
165
+ const req = store.put(doc);
166
+ req.onsuccess = () => resolve();
167
+ req.onerror = () => reject(new Error(`Failed to write entity meta: ${req.error}`));
168
+ });
84
169
  }
85
- // ── Byte ops (BYTES_ENTITY routing) ──────────────────────────────
86
- async _writeBytes(entries) {
87
- const results = [];
88
- // Collect any streams to bytes BEFORE opening the IDB transaction.
89
- // An IDB transaction stays alive only across IDB-callback
90
- // microtasks; awaiting a non-IDB promise (like draining a
91
- // ReadableStream) yields long enough for the transaction to
92
- // auto-commit, after which subsequent put()s either throw
93
- // `TransactionInactiveError` or silently no-op depending on the
94
- // implementation. fake-indexeddb is permissive about this; real
95
- // Chromium is not.
96
- let prepared;
97
- try {
98
- prepared = await Promise.all(entries.map(async (e) => ({
99
- uri: e.uri,
100
- payload: await toBytes(e.payload),
101
- })));
170
+ // ── Write ────────────────────────────────────────────────────────
171
+ async write(meta, entries) {
172
+ if (entries.length === 0)
173
+ return [];
174
+ if ((await this._readMetaSignature(meta.support.entity)) !== meta.signature) {
175
+ return entries.map(({ uri }) => ({
176
+ success: false,
177
+ ...storageFailure(new Error(`${STORE_NAME}: entity '${meta.support.entity}' is not provisioned`), "Entity not provisioned", uri),
178
+ }));
102
179
  }
103
- catch (err) {
104
- const failure = storageFailure(err, "Write failed");
105
- return entries.map(() => ({ success: false, ...failure }));
180
+ // Validate + collect streams BEFORE opening the IDB transaction.
181
+ // IDB transactions stay alive only across IDB-callback microtasks;
182
+ // awaiting a non-IDB promise (like draining a ReadableStream)
183
+ // yields long enough for the transaction to auto-commit.
184
+ const out = new Array(entries.length);
185
+ const prepared = [];
186
+ for (let i = 0; i < entries.length; i++) {
187
+ const { uri, record } = entries[i];
188
+ const extras = Object.keys(record).filter((k) => !meta.declared.has(k));
189
+ if (extras.length > 0) {
190
+ out[i] = {
191
+ success: false,
192
+ ...storageFailure(new Error(`record contains keys not declared in schema '${meta.support.entity}': ${extras.join(", ")}`), "Schema mismatch", uri),
193
+ };
194
+ continue;
195
+ }
196
+ try {
197
+ if (meta.isBytes) {
198
+ const payload = record.payload;
199
+ if (!(payload instanceof Uint8Array) &&
200
+ !(payload instanceof ReadableStream)) {
201
+ throw new Error("BYTES_ENTITY record.payload must be Uint8Array or ReadableStream");
202
+ }
203
+ const bytes = await toBytes(payload);
204
+ prepared.push({ idx: i, doc: { uri, payload: bytes } });
205
+ }
206
+ else {
207
+ const normalised = await this._normaliseBytesFields(record, meta.bytesFields);
208
+ prepared.push({
209
+ idx: i,
210
+ doc: { uri: `${meta.storageRoot}${uri}`, record: normalised },
211
+ });
212
+ }
213
+ }
214
+ catch (err) {
215
+ out[i] = {
216
+ success: false,
217
+ ...storageFailure(err, "Write failed", uri),
218
+ };
219
+ }
106
220
  }
221
+ if (prepared.length === 0)
222
+ return out;
107
223
  try {
108
224
  const store = await this.getStore("readwrite");
109
- for (const record of prepared) {
225
+ for (const { idx, doc } of prepared) {
110
226
  try {
111
227
  await new Promise((resolve, reject) => {
112
- const request = store.put(record);
113
- request.onsuccess = () => resolve();
114
- request.onerror = () => reject(new Error(`Failed to write ${record.uri}: ${request.error}`));
228
+ const req = store.put(doc);
229
+ req.onsuccess = () => resolve();
230
+ req.onerror = () => reject(new Error(`Failed to write ${doc.uri}: ${req.error}`));
115
231
  });
116
- results.push({ success: true });
232
+ out[idx] = { success: true };
117
233
  }
118
234
  catch (err) {
119
- results.push({
235
+ out[idx] = {
120
236
  success: false,
121
- ...storageFailure(err, "Write failed", record.uri),
122
- });
237
+ ...storageFailure(err, "Write failed", entries[idx].uri),
238
+ };
123
239
  }
124
240
  }
125
241
  }
126
242
  catch (err) {
127
- // Outer failure (e.g. db open) — every entry fails with the same
128
- // root cause; no per-entry uri since the failure isn't entry-scoped.
129
243
  const failure = storageFailure(err, "Write failed");
130
- for (const _ of entries) {
131
- results.push({ success: false, ...failure });
244
+ for (const { idx } of prepared) {
245
+ out[idx] = { success: false, ...failure };
132
246
  }
133
247
  }
134
- return results;
248
+ return out;
135
249
  }
136
- _readBytes(urls) {
250
+ // ── Read ─────────────────────────────────────────────────────────
251
+ read(meta, urls) {
137
252
  return dispatchRead(urls, STORE_NAME, {
138
- read: (p) => this._readOne(p.uri),
139
- ls: (p) => this._ls(p),
140
- count: (p) => this._count(p),
253
+ read: (p) => this._readOne(meta, p.uri),
254
+ ls: (p) => this._ls(meta, p),
255
+ count: (p) => this._count(meta, p),
256
+ // v2 §3.5: find walks every descendant under the prefix (no
257
+ // shallow `/` cutoff). With `pushDownFind: false`, dispatch
258
+ // strips pattern/cursor/limit/page before calling, then applies
259
+ // the glob, sort, cursor, limit, format, and appends the cursor-
260
+ // as-trailing-slot on the returned rows. IDB has no engine for
261
+ // glob/regex push-down — only key-range + direction — so the
262
+ // honest shape mirrors memory PR #84: handler returns the raw
263
+ // deep walk, dispatch does the rest.
264
+ find: (p) => this._find(meta, p),
141
265
  });
142
266
  }
143
- async _readOne(uri) {
267
+ async _readOne(meta, uri) {
144
268
  try {
145
269
  const store = await this.getStore();
270
+ const storageKey = meta.isBytes ? uri : `${meta.storageRoot}${uri}`;
146
271
  return await new Promise((resolve) => {
147
- const request = store.get(uri);
148
- request.onsuccess = () => {
149
- const record = request.result;
150
- resolve(record ? record.payload : undefined);
272
+ const req = store.get(storageKey);
273
+ req.onsuccess = () => {
274
+ const doc = req.result;
275
+ if (!doc)
276
+ return resolve(undefined);
277
+ if (meta.isBytes) {
278
+ resolve({ payload: doc.payload });
279
+ }
280
+ else {
281
+ resolve(doc.record);
282
+ }
151
283
  };
152
- request.onerror = () => resolve(undefined);
284
+ req.onerror = () => resolve(undefined);
153
285
  });
154
286
  }
155
287
  catch {
@@ -157,30 +289,37 @@ export class IndexedDBStore {
157
289
  }
158
290
  }
159
291
  /**
160
- * Open a cursor over the `uri_index` constrained to the prefix.
161
- * Returns URIs (and optionally records) that are direct leaves
162
- * under `prefixUri` i.e. `prefixUri + <segment>` with no further
163
- * `/`. Honours sortOrder and limit/page via cursor walking.
292
+ * Cursor over `uri_index` constrained to `[meta.storageRoot+prefix,
293
+ * meta.storageRoot+prefix+￿)`. Honours `sortOrder=desc` via `prev`
294
+ * direction and `limit`/`page` via cursor walking + skipping.
295
+ *
296
+ * `shallow` selects the walk shape:
297
+ * - `true` → only direct leaves (the `rest.includes("/")` tail
298
+ * cutoff). Powers `fn=ls` and shallow `fn=count`.
299
+ * - `false` → every descendant under the prefix, any depth. Powers
300
+ * `fn=find` (v2 §3.5). Dispatch applies the glob, sort, cursor,
301
+ * limit, format on top.
302
+ *
303
+ * Either way the exact-prefix match (`tail === ""`) is skipped — the
304
+ * URL identifies the parent, not a child of itself.
164
305
  */
165
- async _walkLeaves(parsed, onlyUris) {
306
+ async _walkLeaves(meta, parsed, onlyKeys, shallow) {
166
307
  const { uri: prefix, params } = parsed;
167
308
  const desc = params.sortBy === "uri" && params.sortOrder === "desc";
168
309
  const direction = desc ? "prev" : "next";
169
310
  const limit = params.limit;
170
311
  const offset = limit !== undefined ? ((params.page ?? 1) - 1) * limit : 0;
171
- // Bound the cursor to `[prefix, prefix + ￿)` — both ends
172
- // inclusive of the prefix, exclusive of anything past the high
173
- // surrogate. Fall back to a full scan if IDBKeyRange isn't
174
- // available in this environment.
312
+ const lower = `${meta.storageRoot}${prefix}`;
313
+ const upper = `${lower}￿`;
175
314
  const range = this.keyRange
176
- ? this.keyRange.bound(prefix, prefix + "￿", false, false)
315
+ ? this.keyRange.bound(lower, upper, false, false)
177
316
  : undefined;
178
317
  const store = await this.getStore();
179
318
  const index = store.index("uri_index");
180
319
  return await new Promise((resolve, reject) => {
181
320
  const out = [];
182
321
  let skipped = 0;
183
- const request = onlyUris
322
+ const request = onlyKeys
184
323
  ? index.openKeyCursor(range, direction)
185
324
  : index.openCursor(range, direction);
186
325
  request.onsuccess = () => {
@@ -189,13 +328,13 @@ export class IndexedDBStore {
189
328
  resolve(out);
190
329
  return;
191
330
  }
192
- const key = (onlyUris ? cursor.key : cursor.value.uri);
193
- if (!key.startsWith(prefix)) {
331
+ const storedKey = (onlyKeys ? cursor.key : cursor.value.uri);
332
+ if (!storedKey.startsWith(lower)) {
194
333
  resolve(out);
195
334
  return;
196
335
  }
197
- const tail = key.slice(prefix.length);
198
- if (tail === "" || tail.includes("/")) {
336
+ const tail = storedKey.slice(lower.length);
337
+ if (tail === "" || (shallow && tail.includes("/"))) {
199
338
  cursor.continue();
200
339
  return;
201
340
  }
@@ -204,12 +343,10 @@ export class IndexedDBStore {
204
343
  cursor.continue();
205
344
  return;
206
345
  }
207
- if (onlyUris) {
208
- out.push({ uri: key });
209
- }
210
- else {
211
- out.push({ uri: key, payload: cursor.value.payload });
212
- }
346
+ const originalUri = `${prefix}${tail}`;
347
+ out.push(onlyKeys
348
+ ? { uri: originalUri }
349
+ : { uri: originalUri, doc: cursor.value });
213
350
  if (limit !== undefined && out.length >= limit) {
214
351
  resolve(out);
215
352
  return;
@@ -219,42 +356,90 @@ export class IndexedDBStore {
219
356
  request.onerror = () => reject(request.error ?? new Error("Cursor failed"));
220
357
  });
221
358
  }
222
- async _ls(parsed) {
359
+ async _ls(meta, parsed) {
223
360
  validateReadParams(parsed.params, STORE_NAME);
224
361
  const format = parsed.params.format ?? "full";
225
- const onlyUris = format === "uris";
362
+ const onlyKeys = format === "uris";
226
363
  try {
227
- const entries = await this._walkLeaves(parsed, onlyUris);
228
- if (onlyUris)
364
+ const entries = await this._walkLeaves(meta, parsed, onlyKeys, true);
365
+ if (onlyKeys)
229
366
  return entries.map((e) => e.uri);
230
- return entries.map((e) => [e.uri, e.payload]);
367
+ return entries.map((e) => {
368
+ if (!e.doc)
369
+ return [e.uri, undefined];
370
+ if (meta.isBytes) {
371
+ return [e.uri, { payload: e.doc.payload }];
372
+ }
373
+ return [e.uri, e.doc.record];
374
+ });
231
375
  }
232
376
  catch {
233
377
  return [];
234
378
  }
235
379
  }
236
- async _count(parsed) {
380
+ /**
381
+ * v2 §3.5: recursive walk under the prefix. Same cursor as `_ls` with
382
+ * `shallow=false` so the `tail.includes("/")` cutoff is dropped —
383
+ * every descendant flows through, any depth. By the time dispatch
384
+ * calls us (with `pushDownFind: false`), it has stripped
385
+ * pattern/cursor/limit/page from `parsed.params`; sortBy is forced to
386
+ * `"uri"`; format may be either `"full"` or `"uris"`. The handler
387
+ * returns the raw deep walk in that format; dispatch handles glob
388
+ * post-filter, sort, cursor, limit, and the cursor-as-trailing-slot.
389
+ */
390
+ async _find(meta, parsed) {
391
+ validateReadParams(parsed.params, STORE_NAME);
392
+ const format = parsed.params.format ?? "full";
393
+ const onlyKeys = format === "uris";
394
+ try {
395
+ const entries = await this._walkLeaves(meta, parsed, onlyKeys, false);
396
+ if (onlyKeys)
397
+ return entries.map((e) => e.uri);
398
+ return entries.map((e) => {
399
+ if (!e.doc)
400
+ return [e.uri, undefined];
401
+ if (meta.isBytes) {
402
+ return [e.uri, { payload: e.doc.payload }];
403
+ }
404
+ return [e.uri, e.doc.record];
405
+ });
406
+ }
407
+ catch {
408
+ return [];
409
+ }
410
+ }
411
+ async _count(meta, parsed) {
237
412
  if (parsed.params.pattern !== undefined) {
238
413
  throw new Error(`${STORE_NAME}: pattern filter not supported`);
239
414
  }
240
415
  try {
241
- const entries = await this._walkLeaves(parsed, true);
416
+ const entries = await this._walkLeaves(meta, parsed, true, true);
242
417
  return entries.length;
243
418
  }
244
419
  catch {
245
420
  return 0;
246
421
  }
247
422
  }
248
- async _deleteBytes(uris) {
423
+ // ── Delete ───────────────────────────────────────────────────────
424
+ async delete(meta, uris) {
425
+ if (uris.length === 0)
426
+ return [];
427
+ if ((await this._readMetaSignature(meta.support.entity)) !== meta.signature) {
428
+ return uris.map((uri) => ({
429
+ success: false,
430
+ ...storageFailure(new Error(`${STORE_NAME}: entity '${meta.support.entity}' is not provisioned`), "Entity not provisioned", uri),
431
+ }));
432
+ }
249
433
  const results = [];
250
434
  try {
251
435
  const store = await this.getStore("readwrite");
252
436
  for (const uri of uris) {
253
437
  try {
438
+ const storageKey = meta.isBytes ? uri : `${meta.storageRoot}${uri}`;
254
439
  await new Promise((resolve, reject) => {
255
- const request = store.delete(uri);
256
- request.onsuccess = () => resolve();
257
- request.onerror = () => reject(new Error(`Failed to delete ${uri}: ${request.error}`));
440
+ const req = store.delete(storageKey);
441
+ req.onsuccess = () => resolve();
442
+ req.onerror = () => reject(new Error(`Failed to delete ${uri}: ${req.error}`));
258
443
  });
259
444
  results.push({ success: true });
260
445
  }
@@ -267,33 +452,104 @@ export class IndexedDBStore {
267
452
  }
268
453
  }
269
454
  catch (err) {
270
- // Outer failure — every uri fails with the same root cause.
271
455
  const failure = storageFailure(err, "Delete failed");
272
- for (const _ of uris) {
456
+ for (const _ of uris)
273
457
  results.push({ success: false, ...failure });
274
- }
275
458
  }
276
459
  return results;
277
460
  }
278
- // ── Status ───────────────────────────────────────────────────────
461
+ // ── Status / capabilities ────────────────────────────────────────
279
462
  async status() {
280
463
  try {
281
464
  await this.initDB();
465
+ const schema = await this._listProvisionedEntities();
282
466
  return {
283
467
  status: "healthy",
284
- schema: [],
285
- fns: ["read", "ls", "count"],
468
+ schema: schema.map((s) => `entity:${s}`),
469
+ fns: ["read", "ls", "count", "find"],
286
470
  };
287
471
  }
288
472
  catch {
289
473
  return {
290
474
  status: "unhealthy",
291
475
  schema: [],
292
- fns: ["read", "ls", "count"],
476
+ fns: ["read", "ls", "count", "find"],
293
477
  };
294
478
  }
295
479
  }
296
480
  capabilities() {
297
481
  return { atomicBatch: false };
298
482
  }
483
+ // ── Internals ────────────────────────────────────────────────────
484
+ /**
485
+ * Read the persisted signature for an entity's meta record. Returns
486
+ * `null` if no provisioning record exists. The meta record lives at
487
+ * `__meta__/{name}` in the same object store as data records, so a
488
+ * single object-store cursor sees both layouts without needing a
489
+ * separate IDB store.
490
+ */
491
+ async _readMetaSignature(entityName) {
492
+ try {
493
+ const store = await this.getStore();
494
+ return await new Promise((resolve) => {
495
+ const req = store.get(`${META_PREFIX}${entityName}`);
496
+ req.onsuccess = () => {
497
+ const doc = req.result;
498
+ resolve(doc ? doc.signature : null);
499
+ };
500
+ req.onerror = () => resolve(null);
501
+ });
502
+ }
503
+ catch {
504
+ return null;
505
+ }
506
+ }
507
+ /**
508
+ * Scan the meta keyspace for every provisioned entity name. Used by
509
+ * `status()` to advertise the entities a peer can talk to.
510
+ */
511
+ async _listProvisionedEntities() {
512
+ if (!this.keyRange)
513
+ return [];
514
+ const range = this.keyRange.bound(META_PREFIX, `${META_PREFIX}￿`);
515
+ const store = await this.getStore();
516
+ const index = store.index("uri_index");
517
+ return await new Promise((resolve, reject) => {
518
+ const out = [];
519
+ const req = index.openKeyCursor(range, "next");
520
+ req.onsuccess = () => {
521
+ const cursor = req.result;
522
+ if (!cursor)
523
+ return resolve(out);
524
+ const key = cursor.key;
525
+ if (!key.startsWith(META_PREFIX))
526
+ return resolve(out);
527
+ out.push(key.slice(META_PREFIX.length));
528
+ cursor.continue();
529
+ };
530
+ req.onerror = () => reject(req.error ?? new Error("Meta cursor failed"));
531
+ });
532
+ }
533
+ /**
534
+ * Collect any `ReadableStream` values on `bytes`-tagged fields into
535
+ * `Uint8Array` and return a shallow-copied record. Non-stream values
536
+ * pass through; matches the normalisation pattern used by Memory,
537
+ * Postgres, and LocalStorage.
538
+ */
539
+ async _normaliseBytesFields(record, bytesFields) {
540
+ const out = { ...record };
541
+ for (const name of bytesFields) {
542
+ const v = out[name];
543
+ if (v === undefined || v === null)
544
+ continue;
545
+ if (v instanceof Uint8Array)
546
+ continue;
547
+ if (v instanceof ReadableStream) {
548
+ out[name] = await toBytes(v);
549
+ continue;
550
+ }
551
+ throw new Error(`field '${name}' must be Uint8Array or ReadableStream, got ${typeof v}`);
552
+ }
553
+ return out;
554
+ }
299
555
  }
package/esm/ipfs/mod.d.ts CHANGED
@@ -9,6 +9,25 @@
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
  import type { StorePayload } from "../types.js";
14
33
  export interface IpfsExecutor {
@@ -1 +1 @@
1
- {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/ipfs/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC;IAC1D,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,QAAQ,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAClC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC"}
1
+ {"version":3,"file":"mod.d.ts","sourceRoot":"","sources":["../../src/ipfs/mod.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,MAAM,WAAW,YAAY;IAC3B,GAAG,EAAE,CAAC,OAAO,EAAE,YAAY,KAAK,OAAO,CAAC,MAAM,CAAC,CAAC;IAChD,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,cAAc,CAAC,UAAU,CAAC,CAAC,CAAC;IAC1D,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACpC,KAAK,EAAE,CAAC,GAAG,EAAE,MAAM,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACtC,QAAQ,EAAE,MAAM,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IAClC,QAAQ,EAAE,MAAM,OAAO,CAAC,OAAO,CAAC,CAAC;IACjC,OAAO,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC/B;AAED,OAAO,EAAE,SAAS,EAAE,MAAM,YAAY,CAAC"}