wao 0.40.1 → 0.41.0

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 (76) hide show
  1. package/cjs/accounts-web.js +25 -0
  2. package/cjs/accounts.js +38 -0
  3. package/cjs/adaptor-base.js +504 -287
  4. package/cjs/adaptor-cf.js +42 -0
  5. package/cjs/ao-loader.js +1 -1
  6. package/cjs/ao.js +22 -10
  7. package/cjs/aoconnect-base.js +1010 -546
  8. package/cjs/aoconnect-cf.js +24 -0
  9. package/cjs/ar-remote.js +277 -0
  10. package/cjs/armem-base.js +822 -211
  11. package/cjs/armem-cf.js +128 -0
  12. package/cjs/armem.js +11 -5
  13. package/cjs/bar.js +511 -173
  14. package/cjs/car.js +37 -0
  15. package/cjs/cf-env.js +54 -0
  16. package/cjs/cf.js +76 -0
  17. package/cjs/cli.js +12 -6
  18. package/cjs/create.js +1 -1
  19. package/cjs/devs.js +53 -0
  20. package/cjs/dodb.js +116 -0
  21. package/cjs/hb.js +136 -53
  22. package/cjs/hyperbeam.js +85 -44
  23. package/cjs/keygen.js +94 -0
  24. package/cjs/run.js +4 -1
  25. package/cjs/server.js +40 -9
  26. package/cjs/storage-multi.js +525 -0
  27. package/cjs/tgql-d1.js +664 -0
  28. package/cjs/tgql.js +293 -172
  29. package/cjs/workspace/.claude/agents/tester.md +2 -2
  30. package/cjs/workspace/.claude/skills/build/SKILL.md +2 -2
  31. package/cjs/workspace/.claude/skills/test/SKILL.md +3 -2
  32. package/cjs/workspace/CLAUDE.md +2 -2
  33. package/cjs/workspace/README.md +1 -1
  34. package/cjs/workspace/docs/debug.md +9 -4
  35. package/cjs/workspace/docs/hyperbeam-devices.md +50 -1
  36. package/cjs/workspace/docs/wao-sdk.md +1 -1
  37. package/cjs/workspace/package.json +4 -4
  38. package/esm/accounts-web.js +14 -0
  39. package/esm/accounts.js +27 -0
  40. package/esm/adaptor-base.js +129 -31
  41. package/esm/adaptor-cf.js +11 -0
  42. package/esm/ao-loader.js +1 -1
  43. package/esm/ao.js +24 -5
  44. package/esm/aoconnect-base.js +248 -7
  45. package/esm/aoconnect-cf.js +9 -0
  46. package/esm/ar-remote.js +87 -0
  47. package/esm/armem-base.js +304 -53
  48. package/esm/armem-cf.js +67 -0
  49. package/esm/armem.js +7 -2
  50. package/esm/bar.js +126 -16
  51. package/esm/car.js +10 -0
  52. package/esm/cf-env.js +29 -0
  53. package/esm/cf.js +11 -0
  54. package/esm/cli.js +6 -2
  55. package/esm/create.js +1 -1
  56. package/esm/devs.js +15 -0
  57. package/esm/dodb.js +26 -0
  58. package/esm/hb.js +93 -16
  59. package/esm/hyperbeam.js +68 -30
  60. package/esm/keygen.js +47 -0
  61. package/esm/run.js +4 -1
  62. package/esm/server.js +29 -9
  63. package/esm/storage-multi.js +183 -0
  64. package/esm/tgql-d1.js +407 -0
  65. package/esm/tgql.js +29 -10
  66. package/esm/workspace/.claude/agents/tester.md +2 -2
  67. package/esm/workspace/.claude/skills/build/SKILL.md +2 -2
  68. package/esm/workspace/.claude/skills/test/SKILL.md +3 -2
  69. package/esm/workspace/CLAUDE.md +2 -2
  70. package/esm/workspace/README.md +1 -1
  71. package/esm/workspace/docs/debug.md +9 -4
  72. package/esm/workspace/docs/hyperbeam-devices.md +50 -1
  73. package/esm/workspace/docs/wao-sdk.md +1 -1
  74. package/esm/workspace/package.json +4 -4
  75. package/package.json +10 -3
  76. package/postinstall.cjs +84 -0
package/esm/tgql-d1.js ADDED
@@ -0,0 +1,407 @@
1
+ // D1-backed GQL query engine
2
+ // Replaces O(n) scan in tgql.js with indexed SQL queries
3
+
4
+ import { reverse, is, isNil, last, clone, pick, map, includes } from "ramda"
5
+
6
+ const subs = {
7
+ owner: ["address", "key"],
8
+ fee: ["winston", "ar"],
9
+ quantity: ["winston", "ar"],
10
+ data: ["size", "type"],
11
+ tags: ["name", "value"],
12
+ block: ["id", "timestamp", "height", "previous"],
13
+ parent: ["id"],
14
+ bundledIn: ["id"],
15
+ }
16
+
17
+ const field = (key, val = true) => {
18
+ if (includes(key, ["id", "anchor", "signature", "recipient"])) {
19
+ return { key }
20
+ } else if (subs[key]) {
21
+ let _subs = []
22
+ if (val === true) val = subs[key]
23
+ else if (is(Object, val) && !is(Array, val)) {
24
+ let _val = []
25
+ let isTrue = false
26
+ let isFalse = false
27
+ for (const k in val)
28
+ if (val[k] === true) isTrue = true
29
+ else if (val[k] === false) isFalse = true
30
+ if (!isTrue && isFalse) {
31
+ for (const k of subs[key]) if (val[k] !== false) _val.push(k)
32
+ } else {
33
+ for (const k in val) if (val[k] === true) _val.push(k)
34
+ }
35
+ val = _val
36
+ } else if (!is(Array, val)) val = subs[key]
37
+ for (const v2 of val) {
38
+ if (is(String, v2) && includes(v2, subs[key])) _subs.push(v2)
39
+ }
40
+ if (_subs.length === 0) return null
41
+ return { key, subs: _subs }
42
+ }
43
+ return null
44
+ }
45
+
46
+ const field_blocks = (key, val = true) => {
47
+ if (includes(key, ["id", "timestamp", "height", "previous"])) {
48
+ return { key }
49
+ }
50
+ return null
51
+ }
52
+
53
+ function applyFieldProjection(tx, opt) {
54
+ if (isNil(opt.fields)) return tx
55
+
56
+ let _tx2 = { cursor: tx.cursor }
57
+ let fields = []
58
+ if (is(Array, opt.fields) && opt.fields.length > 0) {
59
+ for (const v of opt.fields) {
60
+ if (is(String, v)) {
61
+ const fld = field(v)
62
+ if (fld) fields.push(fld)
63
+ } else if (is(Object, v) && !is(Array, v)) {
64
+ for (const k in v) {
65
+ const fld = field(k, v[k])
66
+ if (fld) fields.push(fld)
67
+ }
68
+ }
69
+ }
70
+ } else if (is(Object, opt.fields) && !is(Array, opt.fields)) {
71
+ for (const k in opt.fields) {
72
+ const fld = field(k, opt.fields[k])
73
+ if (fld) fields.push(fld)
74
+ }
75
+ }
76
+ for (const f of fields) {
77
+ if (isNil(f.subs)) {
78
+ _tx2[f.key] = tx[f.key] ?? ""
79
+ } else {
80
+ _tx2[f.key] = {}
81
+ if (is(Array, tx[f.key])) {
82
+ _tx2[f.key] = map(pick(f.subs))(tx[f.key])
83
+ } else {
84
+ for (const f2 of f.subs) {
85
+ _tx2[f.key][f2] = tx[f.key]?.[f2] ?? ""
86
+ }
87
+ }
88
+ }
89
+ }
90
+ return _tx2
91
+ }
92
+
93
+ export default class D1GQL {
94
+ constructor({ d1, mem }) {
95
+ this.d1 = d1
96
+ this.mem = mem
97
+ }
98
+
99
+ async txs(opt = {}) {
100
+ let _block = {}
101
+ if (is(Number, opt.block)) {
102
+ _block = { min: opt.block, max: opt.block }
103
+ } else if (is(Array, opt.block) && opt.block.length > 0) {
104
+ _block = {}
105
+ if (!isNil(opt.block[0])) _block.min = opt.block[0]
106
+ if (!isNil(opt.block[1])) _block.max = opt.block[1]
107
+ } else if (is(Object, opt.block)) {
108
+ if (!isNil(opt.block.min)) _block.min = opt.block.min
109
+ if (!isNil(opt.block.max)) _block.max = opt.block.max
110
+ }
111
+
112
+ const first = opt.first ?? 10
113
+ let tags = []
114
+ for (const k in opt.tags ?? {}) {
115
+ if (is(String, opt.tags[k])) {
116
+ tags.push({ name: k, values: [opt.tags[k]] })
117
+ } else if (is(Array, opt.tags[k])) {
118
+ tags.push({ name: k, values: opt.tags[k] })
119
+ }
120
+ }
121
+
122
+ let recipients = null
123
+ if (is(Array, opt.recipients)) recipients = opt.recipients
124
+ else if (is(String, opt.recipient)) recipients = [opt.recipient]
125
+
126
+ let owners = null
127
+ if (is(Array, opt.owners)) owners = opt.owners
128
+ else if (is(String, opt.owner)) owners = [opt.owner]
129
+
130
+ let ids = null
131
+ if (is(Array, opt.ids)) ids = opt.ids
132
+ else if (is(String, opt.id)) ids = [opt.id]
133
+
134
+ // Build SQL query
135
+ const conditions = []
136
+ const params = []
137
+
138
+ if (!isNil(_block.min)) {
139
+ conditions.push("t.block_height >= ?")
140
+ params.push(_block.min)
141
+ }
142
+ if (!isNil(_block.max)) {
143
+ conditions.push("t.block_height <= ?")
144
+ params.push(_block.max)
145
+ }
146
+ if (ids && ids.length > 0) {
147
+ conditions.push(`t.id IN (${ids.map(() => "?").join(",")})`)
148
+ params.push(...ids)
149
+ }
150
+ if (owners && owners.length > 0) {
151
+ conditions.push(`t.owner IN (${owners.map(() => "?").join(",")})`)
152
+ params.push(...owners)
153
+ }
154
+ if (recipients && recipients.length > 0) {
155
+ conditions.push(`t.recipient IN (${recipients.map(() => "?").join(",")})`)
156
+ params.push(...recipients)
157
+ }
158
+ if (!isNil(opt.bundledIn) && opt.bundledIn.length > 0) {
159
+ conditions.push(`t.bundle_id IN (${opt.bundledIn.map(() => "?").join(",")})`)
160
+ params.push(...opt.bundledIn)
161
+ }
162
+
163
+ // Tag filters — each tag filter requires ALL to match (AND)
164
+ for (const tag of tags) {
165
+ const valuePlaceholders = tag.values.map(() => "?").join(",")
166
+ conditions.push(
167
+ `t.id IN (SELECT tx_id FROM tx_tags WHERE name = ? AND value IN (${valuePlaceholders}))`
168
+ )
169
+ params.push(tag.name, ...tag.values)
170
+ }
171
+
172
+ // After cursor (pagination)
173
+ if (!isNil(opt.after)) {
174
+ // Get the block_height of the cursor tx to paginate correctly
175
+ const cursorRow = await this.d1
176
+ .prepare("SELECT block_height FROM txs WHERE id = ?")
177
+ .bind(opt.after)
178
+ .first()
179
+ if (cursorRow) {
180
+ if (opt.asc === true) {
181
+ conditions.push("(t.block_height > ? OR (t.block_height = ? AND t.id > ?))")
182
+ params.push(cursorRow.block_height, cursorRow.block_height, opt.after)
183
+ } else {
184
+ conditions.push("(t.block_height < ? OR (t.block_height = ? AND t.id < ?))")
185
+ params.push(cursorRow.block_height, cursorRow.block_height, opt.after)
186
+ }
187
+ }
188
+ }
189
+
190
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""
191
+ const order = opt.asc === true ? "ASC" : "DESC"
192
+
193
+ const sql = `
194
+ SELECT t.id, t.block_id, t.block_height, t.owner, t.recipient,
195
+ t.anchor, t.signature, t.data_size, t.data_type, t.bundle_id, t.parent_id,
196
+ b.height, b.timestamp, b.previous
197
+ FROM txs t
198
+ JOIN blocks b ON t.block_id = b.id
199
+ ${where}
200
+ ORDER BY t.block_height ${order}, t.id ${order}
201
+ LIMIT ?
202
+ `
203
+ params.push(first)
204
+
205
+ const { results: rows } = await this.d1
206
+ .prepare(sql)
207
+ .bind(...params)
208
+ .all()
209
+
210
+ // Fetch tags for all result txs in one query
211
+ const txIds = rows.map(r => r.id)
212
+ let tagMap = {}
213
+ if (txIds.length > 0) {
214
+ const tagSql = `SELECT tx_id, name, value FROM tx_tags WHERE tx_id IN (${txIds.map(() => "?").join(",")})`
215
+ const { results: tagRows } = await this.d1
216
+ .prepare(tagSql)
217
+ .bind(...txIds)
218
+ .all()
219
+ for (const t of tagRows) {
220
+ tagMap[t.tx_id] ??= []
221
+ tagMap[t.tx_id].push({ name: t.name, value: t.value })
222
+ }
223
+ }
224
+
225
+ // Fetch addrmap for owners
226
+ const ownerAddrs = [...new Set(rows.map(r => r.owner).filter(Boolean))]
227
+ let addrMap = {}
228
+ if (ownerAddrs.length > 0) {
229
+ const addrSql = `SELECT address, key FROM addrmap WHERE address IN (${ownerAddrs.map(() => "?").join(",")})`
230
+ const { results: addrRows } = await this.d1
231
+ .prepare(addrSql)
232
+ .bind(...ownerAddrs)
233
+ .all()
234
+ for (const a of addrRows) {
235
+ addrMap[a.address] = { address: a.address, key: a.key }
236
+ }
237
+ }
238
+
239
+ const data = rows.map(row => {
240
+ let _tx = {
241
+ cursor: row.id,
242
+ id: row.id,
243
+ recipient: row.recipient || "",
244
+ tags: tagMap[row.id] || [],
245
+ parent: row.parent_id ? { id: row.parent_id } : { id: "" },
246
+ bundledIn: row.bundle_id ? { id: row.bundle_id } : { id: "" },
247
+ block: {
248
+ id: row.block_id,
249
+ timestamp: row.timestamp,
250
+ height: row.height,
251
+ previous: row.previous || "",
252
+ },
253
+ anchor: row.anchor || "",
254
+ signature: row.signature || "",
255
+ owner: addrMap[row.owner] || { address: row.owner, key: "" },
256
+ fee: { ar: "0.000000000000", winston: "0" },
257
+ quantity: { ar: "0.000000000000", winston: "0" },
258
+ data: { size: row.data_size || "0", type: row.data_type || "" },
259
+ }
260
+ return applyFieldProjection(_tx, opt)
261
+ })
262
+
263
+ if (opt.next === true) {
264
+ let cursor = null
265
+ if (data.length > 0) cursor = last(data).cursor
266
+ const next = !cursor
267
+ ? null
268
+ : async () => {
269
+ let _opt = clone(opt)
270
+ _opt.after = cursor
271
+ return await this.txs(_opt)
272
+ }
273
+ return { data, next }
274
+ }
275
+ return data
276
+ }
277
+
278
+ async blocks(opt = {}) {
279
+ let _block = {}
280
+ if (is(Number, opt.block)) {
281
+ _block = { min: opt.block, max: opt.block }
282
+ } else if (is(Array, opt.block) && opt.block.length > 0) {
283
+ _block = {}
284
+ if (!isNil(opt.block[0])) _block.min = opt.block[0]
285
+ if (!isNil(opt.block[1])) _block.max = opt.block[1]
286
+ }
287
+ const first = opt.first ?? 20
288
+
289
+ let ids = null
290
+ if (is(Array, opt.ids)) ids = opt.ids
291
+ else if (is(String, opt.id)) ids = [opt.id]
292
+
293
+ const conditions = []
294
+ const params = []
295
+
296
+ if (!isNil(_block.min)) {
297
+ conditions.push("height >= ?")
298
+ params.push(_block.min)
299
+ }
300
+ if (!isNil(_block.max)) {
301
+ conditions.push("height <= ?")
302
+ params.push(_block.max)
303
+ }
304
+ if (ids && ids.length > 0) {
305
+ conditions.push(`id IN (${ids.map(() => "?").join(",")})`)
306
+ params.push(...ids)
307
+ }
308
+
309
+ if (!isNil(opt.after)) {
310
+ const cursorRow = await this.d1
311
+ .prepare("SELECT height FROM blocks WHERE id = ?")
312
+ .bind(opt.after)
313
+ .first()
314
+ if (cursorRow) {
315
+ if (opt.asc === true) {
316
+ conditions.push("height > ?")
317
+ params.push(cursorRow.height)
318
+ } else {
319
+ conditions.push("height < ?")
320
+ params.push(cursorRow.height)
321
+ }
322
+ }
323
+ }
324
+
325
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : ""
326
+ const order = opt.asc === true ? "ASC" : "DESC"
327
+
328
+ const sql = `
329
+ SELECT id, height, timestamp, previous
330
+ FROM blocks
331
+ ${where}
332
+ ORDER BY height ${order}
333
+ LIMIT ?
334
+ `
335
+ params.push(first)
336
+
337
+ const { results: rows } = await this.d1
338
+ .prepare(sql)
339
+ .bind(...params)
340
+ .all()
341
+
342
+ let data = rows.map(row => {
343
+ let block = {
344
+ cursor: row.id,
345
+ id: row.id,
346
+ height: row.height,
347
+ timestamp: row.timestamp,
348
+ previous: row.previous || "",
349
+ }
350
+
351
+ // Apply field projection
352
+ if (!isNil(opt.fields)) {
353
+ let block2 = { cursor: block.id }
354
+ let fields = []
355
+ if (is(Array, opt.fields) && opt.fields.length > 0) {
356
+ for (const v of opt.fields) {
357
+ if (is(String, v)) {
358
+ const fld = field_blocks(v)
359
+ if (fld) fields.push(fld)
360
+ } else if (is(Object, v) && !is(Array, v)) {
361
+ for (const k in v) {
362
+ const fld = field_blocks(k, v[k])
363
+ if (fld) fields.push(fld)
364
+ }
365
+ }
366
+ }
367
+ } else if (is(Object, opt.fields) && !is(Array, opt.fields)) {
368
+ for (const k in opt.fields) {
369
+ const fld = field_blocks(k, opt.fields[k])
370
+ if (fld) fields.push(fld)
371
+ }
372
+ }
373
+ for (const f of fields) {
374
+ if (isNil(f.subs)) {
375
+ block2[f.key] = block[f.key] ?? ""
376
+ } else {
377
+ block2[f.key] = {}
378
+ if (is(Array, block[f.key])) {
379
+ block2[f.key] = map(pick(f.subs))(block[f.key])
380
+ } else {
381
+ for (const f2 of f.subs) {
382
+ block2[f.key][f2] = block[f.key]?.[f2] ?? ""
383
+ }
384
+ }
385
+ }
386
+ }
387
+ block = block2
388
+ }
389
+
390
+ return block
391
+ })
392
+
393
+ if (opt.next === true) {
394
+ let cursor = null
395
+ if (data.length > 0) cursor = last(data).cursor
396
+ const next = !cursor
397
+ ? null
398
+ : async () => {
399
+ let _opt = clone(opt)
400
+ _opt.after = cursor
401
+ return await this.txs(_opt)
402
+ }
403
+ return { data, next }
404
+ }
405
+ return data
406
+ }
407
+ }
package/esm/tgql.js CHANGED
@@ -1,3 +1,4 @@
1
+ import D1GQL from "./tgql-d1.js"
1
2
  import { reverse, includes, map, is, isNil, last, clone, pick } from "ramda"
2
3
 
3
4
  const subs = {
@@ -48,10 +49,20 @@ const field_blocks = (key, val = true) => {
48
49
  }
49
50
 
50
51
  export default class GQL {
51
- constructor({ mem }) {
52
+ constructor({ mem, d1 }) {
52
53
  this.mem = mem
54
+ this.d1 = d1 || null
55
+ if (this.d1) {
56
+ this._d1gql = new D1GQL({ d1: this.d1, mem: this.mem })
57
+ }
53
58
  }
54
59
  async txs(opt = {}) {
60
+ if (this._d1gql) {
61
+ try { return await this._d1gql.txs(opt) } catch (e) {
62
+ // Fall through to O(n) scan only if D1 schema is missing
63
+ if (!e?.message?.includes("no such table")) throw e
64
+ }
65
+ }
55
66
  let block_max = null
56
67
  let block_min = null
57
68
  let _block = {}
@@ -91,7 +102,7 @@ export default class GQL {
91
102
  let count = 0
92
103
  let after = false
93
104
  for (const v of blocks) {
94
- const block = clone(this.mem.blockmap[v])
105
+ const block = clone(await this.mem.get("blockmap", v))
95
106
  if (!isNil(_block.min) && block.height < _block.min) continue
96
107
  if (!isNil(_block.max) && block.height > _block.max) continue
97
108
  let txs = block.txs
@@ -103,6 +114,7 @@ export default class GQL {
103
114
  }
104
115
  if (!isNil(opt.after) && count === 0 && !after) continue
105
116
  let tx = await this.mem.getTx(v2)
117
+ if (!tx) continue
106
118
  if (!isNil(ids) && ids.length > 0) {
107
119
  if (!includes(tx.id, ids)) continue
108
120
  }
@@ -113,10 +125,13 @@ export default class GQL {
113
125
  if (!isNil(recipients) && recipients.length > 0) {
114
126
  if (!includes(tx.recipient, recipients)) continue
115
127
  }
128
+ if (!isNil(opt.bundledIn) && opt.bundledIn.length > 0) {
129
+ if (!tx.bundledIn?.id || !includes(tx.bundledIn.id, opt.bundledIn)) continue
130
+ }
116
131
  let tag_unmatch = false
117
132
  for (const v of tags) {
118
133
  let ex = false
119
- for (const v2 of tx.tags) {
134
+ for (const v2 of (tx.tags || [])) {
120
135
  if (v2.name === v.name && includes(v2.value, v.values)) {
121
136
  ex = true
122
137
  break
@@ -132,17 +147,16 @@ export default class GQL {
132
147
  cursor: tx.id,
133
148
  id: tx.id,
134
149
  recipient: tx.recipient ?? "",
135
- data: tx._data,
136
- tags: tx.tags,
150
+ tags: tx.tags || [],
137
151
  parent: tx.parent ?? { id: "" },
138
152
  bundledIn: tx.bundledIn ?? { id: "" },
139
153
  block: pick(["id", "timestamp", "height", "previous"], block),
140
154
  anchor: tx.anchor ?? "",
141
155
  signature: tx.signature ?? "",
142
- owner: { address: tx.owner, key: this.mem.addrmap[tx.owner] },
156
+ owner: { address: tx.owner, key: await this.mem.get("addrmap", tx.owner) },
143
157
  fee: { ar: "0.000000000000", winston: "0" },
144
158
  quantity: { ar: "0.000000000000", winston: "0" },
145
- data: { size: "0", type: "" },
159
+ data: tx._data?.type ? tx._data : { size: tx._data?.size || tx.data_size || (tx.data ? String(tx.data.length) : "0"), type: (tx.tags || []).find(t => t.name === "Content-Type")?.value || "" },
146
160
  }
147
161
  if (!isNil(opt.fields)) {
148
162
  let _tx2 = { cursor: tx.id }
@@ -173,7 +187,7 @@ export default class GQL {
173
187
  _tx2[f.key] = {}
174
188
  if (is(Array, _tx[f.key])) {
175
189
  _tx2[f.key] = map(pick(f.subs))(_tx[f.key])
176
- } else {
190
+ } else if (_tx[f.key] != null) {
177
191
  for (const f2 of f.subs) {
178
192
  _tx2[f.key][f2] = _tx[f.key][f2] ?? ""
179
193
  }
@@ -204,6 +218,11 @@ export default class GQL {
204
218
  }
205
219
  }
206
220
  async blocks(opt = {}) {
221
+ if (this._d1gql) {
222
+ try { return await this._d1gql.blocks(opt) } catch (e) {
223
+ if (!e?.message?.includes("no such table")) throw e
224
+ }
225
+ }
207
226
  let block_max = null
208
227
  let block_min = null
209
228
  let _block = {}
@@ -226,7 +245,7 @@ export default class GQL {
226
245
  let count = 0
227
246
  let after = false
228
247
  for (const v of blocks) {
229
- let block = clone({ ...this.mem.blockmap[v], cursor: v })
248
+ let block = clone({ ...(await this.mem.get("blockmap", v)), cursor: v })
230
249
  delete block.txs
231
250
  if (!isNil(_block.min) && block.height < _block.min) continue
232
251
  if (!isNil(_block.max) && block.height > _block.max) continue
@@ -260,7 +279,7 @@ export default class GQL {
260
279
  block2[f.key] = {}
261
280
  if (is(Array, block[f.key])) {
262
281
  block2[f.key] = map(pick(f.subs))(block[f.key])
263
- } else {
282
+ } else if (block[f.key] != null) {
264
283
  for (const f2 of f.subs) {
265
284
  block2[f.key][f2] = block[f.key][f2] ?? ""
266
285
  }
@@ -29,7 +29,7 @@ yarn test # all tests
29
29
  yarn test test/aos.test.js # specific file
30
30
  ```
31
31
 
32
- The test command runs: `node --experimental-wasm-memory64 --test --test-concurrency=1`
32
+ The test command runs: `node --test --test-concurrency=1` (Node 24+ enables wasm-memory64 by default and rejects the experimental flag; Node 22 users add `--experimental-wasm-memory64` via NODE_OPTIONS).
33
33
 
34
34
  ## Systematic Debugging
35
35
 
@@ -59,7 +59,7 @@ Report test results with specifics: which tests pass, which fail, and the error
59
59
  ## Common Issues
60
60
 
61
61
  - **Port already in use**: Kill stale `beam.smp` processes
62
- - **WASM memory error**: Ensure `--experimental-wasm-memory64` flag is present
62
+ - **WASM memory error**: On Node 22 set `NODE_OPTIONS=--experimental-wasm-memory64`. On Node 24+ the flag is default-on (and rejected if passed explicitly — "bad option: --experimental-wasm-memory64")
63
63
  - **Process not found**: Check that `src_data` path is correct
64
64
  - **HyperBEAM timeout**: Ensure `hbeam.kill()` is called in `after()`
65
65
  - **Send().receive() hangs**: Does NOT work on genesis-wasm — use fire-and-forget `Send()` + separate Handlers.add calls
@@ -210,5 +210,5 @@ Never force a task to `done` status with failing tests. The TaskCompleted hook w
210
210
  - Verify task statuses — a task stuck as `in_progress` blocks progression
211
211
 
212
212
  ### Permission denied on yarn test
213
- - Check `node --version` is 20+
214
- - Verify `--experimental-wasm-memory64` flag is present in package.json
213
+ - Check `node --version` is 22+ (24+ recommended)
214
+ - On Node 22, set `NODE_OPTIONS=--experimental-wasm-memory64`; on Node 24+ the flag is default-on and is rejected if passed explicitly
@@ -51,5 +51,6 @@ yarn test
51
51
  - Check `package.json` has `"wao"` in dependencies
52
52
 
53
53
  ### WASM memory error
54
- - Ensure `--experimental-wasm-memory64` flag is in the test command
55
- - Node.js 20+ required
54
+ - Node 24+ enables wasm-memory64 by default — DO NOT pass `--experimental-wasm-memory64` (it's rejected with "bad option")
55
+ - Node 22: set `NODE_OPTIONS=--experimental-wasm-memory64` before running
56
+ - Node.js 22+ required
@@ -95,7 +95,7 @@ yarn deploy --mainnet # remote HyperBEAM (push-1)
95
95
  yarn deploy --mainnet --lua # remote HyperBEAM (Lua mode)
96
96
  ```
97
97
 
98
- Runs: `node --experimental-wasm-memory64 --test --test-concurrency=1`
98
+ Runs: `node --test --test-concurrency=1` (Node 24+; on Node 22 prefix with `--experimental-wasm-memory64`)
99
99
 
100
100
  ## Deployment Targets
101
101
 
@@ -109,7 +109,7 @@ Runs: `node --experimental-wasm-memory64 --test --test-concurrency=1`
109
109
  - **Remote nodes**: Use `push-1` through `push-10` for full compute. `push.forward.computer` is push-only (no compute).
110
110
  - **Lua mode**: Faster but no `receive()` — use `msg.reply()` pattern instead.
111
111
  - **Wallet**: Run `yarn keygen` to generate `.wallet.json`.
112
- - **HyperBEAM fork**: `git clone -b wao-beta3 https://github.com/weavedb/HyperBEAM.git`
112
+ - **HyperBEAM fork**: `git clone -b wao-final https://github.com/weavedb/HyperBEAM.git`
113
113
 
114
114
  ### Frontend Commands
115
115
 
@@ -52,7 +52,7 @@ yarn deploy --local-hb --lua src/counter.lua
52
52
 
53
53
  **Remote nodes**: Use `push-1` through `push-10` for full compute. `push.forward.computer` is push-only (no compute).
54
54
 
55
- **HyperBEAM fork**: `git clone -b wao-beta3 https://github.com/weavedb/HyperBEAM.git && cd HyperBEAM && rebar3 compile`
55
+ **HyperBEAM fork**: `git clone -b wao-final https://github.com/weavedb/HyperBEAM.git && cd HyperBEAM && rebar3 compile`
56
56
 
57
57
  ---
58
58
 
@@ -144,13 +144,17 @@ await hb.schedule({
144
144
 
145
145
  **Problem:** `WebAssembly.Memory` errors or WASM module fails to load.
146
146
 
147
- **Fix:** Ensure the `--experimental-wasm-memory64` flag is present:
147
+ **Fix:** Node 24+ enables wasm-memory64 by default (and rejects the experimental flag at startup with "bad option"). On Node 22 or older, prefix with `--experimental-wasm-memory64`:
148
148
 
149
149
  ```bash
150
+ # Node 24+: no flag needed
151
+ node --test test/aos.test.js
152
+
153
+ # Node 22:
150
154
  node --experimental-wasm-memory64 --test test/aos.test.js
151
155
  ```
152
156
 
153
- The `yarn test` script should already include this flag.
157
+ The `yarn test` script ships without the flag (Node 24+ default); set `NODE_OPTIONS=--experimental-wasm-memory64` if you're on older Node.
154
158
 
155
159
  ### Genesis-WASM Server
156
160
 
@@ -202,7 +206,8 @@ The `yarn test` script should already include this flag.
202
206
  | `badarg in list_to_atom` | Atom not pre-registered | Add to preRegisterAtoms |
203
207
  | `multiple_matches` | Duplicate message content | Add nonce to messages |
204
208
  | `timeout` | Process or server hung | Kill stale processes, increase timeout |
205
- | `WASM memory error` | Missing memory64 flag | Add `--experimental-wasm-memory64` |
209
+ | `WASM memory error` | Missing memory64 (Node 22) | Set `NODE_OPTIONS=--experimental-wasm-memory64` (Node 22) — Node 24+ has it default-on |
210
+ | `bad option: --experimental-wasm-memory64` | Passing the flag on Node 24+ | Remove the flag — Node 24+ rejects it |
206
211
  | `Process not found` | Bad pid or deploy failed | Check deploy result, verify pid |
207
212
  | `Insufficient balance` | Payment balance too low | Top up via simple-pay or p4 |
208
213
  | `Not signed` | Missing wallet/JWK | Call `.init(jwk)` before operations |
@@ -215,7 +220,7 @@ When tests fail, check in order:
215
220
 
216
221
  1. **Ports clear?** `lsof -ti :10000-10010 | xargs -r kill -9`
217
222
  2. **beam.smp killed?** `pkill -f beam.smp`
218
- 3. **WASM flag?** `--experimental-wasm-memory64` in test command
223
+ 3. **WASM flag?** Node 24+ default-on; on Node 22 add `--experimental-wasm-memory64` (and on Node 24+ remove it — it's rejected)
219
224
  4. **HyperBEAM dir?** `ls ./HyperBEAM` exists
220
225
  5. **Wallet exists?** `.wallet.json` present (auto-generated)
221
226
  6. **hbeam.kill() in after()?** Always clean up
@@ -348,7 +348,7 @@ const result = await hb.computeLegacy({ pid, slot })
348
348
  **Limitations:**
349
349
  - External CU is single-pass — **`Send().receive()` does NOT work**
350
350
  - Auto-starts CU server at port 6363
351
- - Requires `--experimental-wasm-memory64` Node.js flag
351
+ - Requires wasm-memory64 (Node 24+ default; Node 22 needs `--experimental-wasm-memory64`)
352
352
 
353
353
  Functions: `init/3`, `compute/3`, `snapshot/3`, `import/3`
354
354
 
@@ -540,6 +540,55 @@ Query Arweave via GraphQL from within HyperBEAM.
540
540
 
541
541
  ---
542
542
 
543
+ ## v0.9-FINAL Devices (new since beta3)
544
+
545
+ ### location@1.0 — Scheduler-Location Registry
546
+
547
+ Replaces the old `/~scheduler@1.0/location` endpoint. Stores per-address scheduler-location records (with DNS/IP resolution and a TTL) in a node-local cache, with fallback to the Arweave gateway.
548
+
549
+ - `POST /~location@1.0/node` — operator generates and registers a record (signed)
550
+ - `POST /~location@1.0/known` — cache a peer's record if newer than the local one
551
+ - Read via `dev_whois` / `dev_location_cache`
552
+
553
+ ### metering@1.0 — Dynamic P4 Pricing
554
+
555
+ A P4 pricing device that records resource usage per request/response lifecycle.
556
+
557
+ - `estimate/3` opens a process-local session
558
+ - `consume/3` increments usage during the session
559
+ - `price/3` closes the session and returns the integer charge
560
+ - Operator sets `metering-rates` in node message (resource → AO units per resource unit)
561
+
562
+ ### blacklist@1.0 — Content Moderation by Blacklist
563
+
564
+ A request hook for filtering. Operator configures `blacklist-providers` (list) in node message; each provider returns a moderation set.
565
+
566
+ ### bundler@1.0 — ANS-104 Bundler Integration
567
+
568
+ Outbound bundler for posting ANS-104 transactions to upstream Arweave bundlers (e.g. `up.arweave.net`).
569
+
570
+ ### gzip@1.0 — gzip Codec
571
+
572
+ Compress/decompress message bodies via gzip during processing.
573
+
574
+ ### rate-limit@1.0 — Per-Address Rate Limiting
575
+
576
+ Operator-configurable per-address rate limits applied as a request hook.
577
+
578
+ ### tx@1.0 — Arweave TX Helpers
579
+
580
+ Helpers for fetching and validating Arweave transactions inline.
581
+
582
+ ### b32-name@1.0 — Base-32 Name Encoding
583
+
584
+ Helpers for base-32 name encoding/decoding used by other devices.
585
+
586
+ ### arweave@2.9 — Arweave Block + GraphQL Integration
587
+
588
+ Renamed in v0.9-FINAL (was `arweave@2.9-pre`). Provides Arweave block cache, offset helpers, and common Arweave protocol primitives.
589
+
590
+ ---
591
+
543
592
  ## Multi-Instance Setup
544
593
 
545
594
  Running multiple HyperBEAM nodes requires unique ports and Erlang node names: