hbsig 0.2.8 → 0.3.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.
package/esm/structured.js CHANGED
@@ -73,7 +73,7 @@ function to(tabm) {
73
73
  // Build result with empty values first
74
74
  const result = {}
75
75
 
76
- // Add empty values based on their types
76
+ // Add empty values based on their types (these may be overwritten if data exists)
77
77
  for (const [key, type] of Object.entries(types)) {
78
78
  if (type === "empty-binary") {
79
79
  result[key] = ""
@@ -111,11 +111,19 @@ function to(tabm) {
111
111
  value !== null &&
112
112
  !Array.isArray(value)
113
113
  ) {
114
+ // Check if the child object itself indicates it's a list via .="list" in its ao-types
115
+ const childAoTypes = value["ao-types"] || ""
116
+ const childTypes = parseAoTypes(childAoTypes)
117
+ const isChildList = childTypes["."] === "list"
118
+
114
119
  // Recursively decode child TABM
115
120
  const childDecoded = to(value)
116
- const type = types[normalizedKey]
117
121
 
118
- if (type === "list") {
122
+ // Only convert numbered map to array if the child object itself
123
+ // declares it's a list via .="list" in its ao-types.
124
+ // This preserves original keys when the parent declares the type
125
+ // but the child doesn't have the list marker.
126
+ if (isChildList) {
119
127
  // Convert numbered map back to ordered list
120
128
  result[rawKey] = messageToOrderedList(childDecoded)
121
129
  } else {
@@ -206,9 +214,23 @@ function decodeValue(type, value) {
206
214
  case "float":
207
215
  return parseFloat(value)
208
216
 
217
+ case "boolean":
218
+ // SF boolean format: ?1 = true, ?0 = false
219
+ // Convert to native boolean, will be encoded as "atom" type
220
+ if (value === "?1") return true
221
+ if (value === "?0") return false
222
+ // Fallback for other formats
223
+ return value === "true" || value === "1"
224
+
209
225
  case "atom":
210
226
  const atomItem = parseStructuredItem(value)
211
- return atomItem.replace(/^"|"$/g, "") // Remove quotes
227
+ const atomName = atomItem.replace(/^"|"$/g, "") // Remove quotes
228
+ // Convert to Symbol to preserve atom type through round-trip
229
+ // Special cases for common atoms that JS has native types for
230
+ if (atomName === "true") return true
231
+ if (atomName === "false") return false
232
+ if (atomName === "null") return null
233
+ return Symbol.for(atomName)
212
234
 
213
235
  case "list":
214
236
  return parseStructuredList(value).map(item => {
@@ -236,7 +258,8 @@ function decodeValue(type, value) {
236
258
  * @returns {*} - Parsed value
237
259
  */
238
260
  function parseStructuredItem(value) {
239
- // This is a simplified parser - you'd want to use a proper structured fields parser
261
+ // Handle non-string values (e.g., numbers from HyperBEAM responses)
262
+ if (typeof value !== "string") return String(value)
240
263
  if (value.startsWith('"') && value.endsWith('"')) {
241
264
  return value.slice(1, -1) // Remove quotes
242
265
  }
@@ -249,10 +272,14 @@ function parseStructuredItem(value) {
249
272
  * @returns {Array} - Parsed list
250
273
  */
251
274
  function parseStructuredList(value) {
252
- // This is a simplified parser - you'd want to use a proper structured fields parser
275
+ if (typeof value !== "string") return [value]
253
276
  return value.split(", ").map(item => {
254
277
  if (item.startsWith('"') && item.endsWith('"')) {
255
- return item.slice(1, -1) // Remove quotes
278
+ // Remove quotes and unescape SF string escapes
279
+ // In SF strings: \" = " and \\ = \
280
+ return item.slice(1, -1)
281
+ .replace(/\\"/g, '"') // \" -> "
282
+ .replace(/\\\\/g, '\\') // \\ -> \
256
283
  }
257
284
  return item
258
285
  })
@@ -264,44 +291,72 @@ function parseStructuredList(value) {
264
291
  * @returns {object} - TABM
265
292
  */
266
293
  function from(msg) {
267
- // Handle non-map values
268
- if (
269
- msg instanceof Buffer ||
270
- typeof msg !== "object" ||
271
- msg === null ||
272
- Array.isArray(msg)
273
- ) {
294
+ // Handle binary input - passthrough
295
+ if (msg instanceof Buffer || msg instanceof Uint8Array) {
296
+ return msg
297
+ }
298
+
299
+ // Handle non-object values - passthrough
300
+ if (typeof msg !== "object" || msg === null) {
274
301
  return msg
275
302
  }
276
303
 
277
- // Normalize keys first
278
- const normalizedMap = {}
304
+ // Handle arrays - convert to numbered map with .="list" in ao-types
305
+ // Mirrors Erlang: from(List, Req, Opts) when is_list(List)
306
+ if (Array.isArray(msg)) {
307
+ // Convert to numbered map (1-based indexing like Erlang)
308
+ const numberedMap = {}
309
+ msg.forEach((item, idx) => {
310
+ numberedMap[(idx + 1).toString()] = item
311
+ })
312
+
313
+ // Recursively process the numbered map
314
+ const result = from(numberedMap)
315
+
316
+ // Add .="list" to ao-types to indicate this message is a list
317
+ const existingAoTypes = result["ao-types"] || ""
318
+ if (existingAoTypes) {
319
+ result["ao-types"] = '.="list", ' + existingAoTypes
320
+ } else {
321
+ result["ao-types"] = '.="list"'
322
+ }
323
+
324
+ return result
325
+ }
326
+
327
+ // Process keys - preserve original case to match Erlang behavior
328
+ // HTTP headers are case-insensitive but JSON/map keys preserve case
329
+ const keysMap = {}
279
330
  for (const [key, value] of Object.entries(msg)) {
280
- const normKey = key.toLowerCase()
281
- normalizedMap[normKey] = value
331
+ keysMap[key] = value
282
332
  }
283
333
 
284
- // Get sorted keys (normalized)
285
- const sortedKeys = Object.keys(normalizedMap).sort()
334
+ // Get sorted keys (preserving case)
335
+ const sortedKeys = Object.keys(keysMap).sort()
286
336
 
287
337
  const types = []
288
338
  const values = []
289
339
 
290
340
  // Process each key in sorted order
291
- for (const normKey of sortedKeys) {
292
- const value = normalizedMap[normKey]
341
+ for (const key of sortedKeys) {
342
+ const value = keysMap[key]
293
343
 
294
- // Handle empty values
344
+ // Handle empty binaries/strings - just include as-is, no type annotation
345
+ // (Erlang doesn't add empty-binary type, it just keeps the empty binary)
295
346
  if (value === "" || (value instanceof Buffer && value.length === 0)) {
296
- types.push([normKey, "empty-binary"])
347
+ values.push([key, value])
297
348
  continue
298
349
  }
299
350
 
351
+ // Empty arrays - convert to numbered map with .="list" in ao-types
352
+ // (Erlang doesn't add empty-list type, just the list marker)
300
353
  if (Array.isArray(value) && value.length === 0) {
301
- types.push([normKey, "empty-list"])
354
+ values.push([key, from(value)])
302
355
  continue
303
356
  }
304
357
 
358
+ // Empty objects - just include as-is, no type annotation
359
+ // (Erlang doesn't add empty-message type, it just keeps the empty map)
305
360
  if (
306
361
  typeof value === "object" &&
307
362
  value !== null &&
@@ -309,43 +364,30 @@ function from(msg) {
309
364
  !(value instanceof Buffer) &&
310
365
  Object.keys(value).length === 0
311
366
  ) {
312
- types.push([normKey, "empty-message"])
367
+ values.push([key, value])
313
368
  continue
314
369
  }
315
370
 
316
371
  // Handle binary/string values
317
372
  if (value instanceof Buffer || value instanceof Uint8Array) {
318
- values.push([normKey, value])
373
+ values.push([key, value])
319
374
  continue
320
375
  }
321
376
 
322
377
  if (typeof value === "string") {
323
- values.push([normKey, value])
378
+ values.push([key, value])
324
379
  continue
325
380
  }
326
381
 
327
382
  // Handle nested maps
328
383
  if (typeof value === "object" && !Array.isArray(value) && value !== null) {
329
- values.push([normKey, from(value)])
384
+ values.push([key, from(value)])
330
385
  continue
331
386
  }
332
387
 
333
- // Handle arrays
388
+ // Handle arrays - from() converts to numbered map with .="list" in ao-types
334
389
  if (Array.isArray(value) && value.length > 0) {
335
- if (shouldConvertToNumberedMap(value)) {
336
- // Convert to numbered map (1-based indexing)
337
- const numberedMap = {}
338
- value.forEach((item, idx) => {
339
- numberedMap[(idx + 1).toString()] = item
340
- })
341
- types.push([normKey, "list"])
342
- values.push([normKey, from(numberedMap)])
343
- } else {
344
- // Encode as list string
345
- const [type, encoded] = encodeValue(value)
346
- types.push([normKey, type])
347
- values.push([normKey, encoded])
348
- }
390
+ values.push([key, from(value)])
349
391
  continue
350
392
  }
351
393
 
@@ -353,13 +395,12 @@ function from(msg) {
353
395
  if (
354
396
  typeof value === "symbol" ||
355
397
  typeof value === "number" ||
356
- Array.isArray(value) ||
357
398
  typeof value === "boolean" ||
358
399
  value === null
359
400
  ) {
360
401
  const [type, encoded] = encodeValue(value)
361
- types.push([normKey, type])
362
- values.push([normKey, encoded])
402
+ types.push([key, type])
403
+ values.push([key, encoded])
363
404
  continue
364
405
  }
365
406
  }
@@ -380,54 +421,13 @@ function from(msg) {
380
421
  return result
381
422
  }
382
423
 
383
- /**
384
- * Check if an array should be converted to numbered map
385
- * Rules based on Erlang behavior:
386
- * 1. Contains any objects/maps → convert
387
- * 2. Contains empty arrays (but NOT empty buffers) → convert
388
- * 3. All items are arrays (array of arrays) → convert
389
- * 4. Otherwise → encode as string
390
- */
391
- function shouldConvertToNumberedMap(arr) {
392
- let allArrays = true
393
- let hasObjects = false
394
- let hasEmptyArrays = false
395
-
396
- for (const item of arr) {
397
- // Check for objects (not arrays or buffers)
398
- if (
399
- typeof item === "object" &&
400
- item !== null &&
401
- !Array.isArray(item) &&
402
- !Buffer.isBuffer(item)
403
- ) {
404
- hasObjects = true
405
- }
406
- // Check for empty arrays only (NOT empty buffers)
407
- else if (Array.isArray(item) && item.length === 0) {
408
- hasEmptyArrays = true
409
- }
410
- // Track if all items are arrays
411
- else if (!Array.isArray(item)) {
412
- allArrays = false
413
- }
414
- }
415
-
416
- // Convert if: has objects, has empty arrays, or all items are non-empty arrays
417
- return (
418
- hasObjects ||
419
- hasEmptyArrays ||
420
- (allArrays && arr.length > 0 && arr.every(item => Array.isArray(item)))
421
- )
422
- }
423
-
424
424
  /**
425
425
  * Encode a value with its type
426
426
  */
427
427
  function encodeValue(value) {
428
- // Null (as atom)
428
+ // Null (as atom) - use token format (unquoted)
429
429
  if (value === null) {
430
- return ["atom", '"null"']
430
+ return ["atom", "null"]
431
431
  }
432
432
 
433
433
  // Integer
@@ -437,24 +437,23 @@ function encodeValue(value) {
437
437
 
438
438
  // Float
439
439
  if (typeof value === "number") {
440
- // Format like Erlang with scientific notation
440
+ // Format like Erlang's float_to_binary - scientific notation with full precision
441
+ // Erlang's float_to_binary/1 uses ~20 decimal digits and keeps trailing zeros
441
442
  let str = value.toExponential(20)
442
- // Remove trailing zeros but keep at least one
443
- str = str.replace(/(\.\d*?)0+e/, "$1e").replace(/\.e/, ".0e")
444
- // Ensure 2-digit exponent
443
+ // Ensure 2-digit exponent with sign
445
444
  str = str.replace(/e([+-])(\d)$/, "e$10$2")
446
445
  return ["float", str]
447
446
  }
448
447
 
449
- // Boolean (as atom)
448
+ // Boolean (as atom) - use token format (unquoted)
450
449
  if (typeof value === "boolean") {
451
- return ["atom", `"${value}"`]
450
+ return ["atom", value.toString()]
452
451
  }
453
452
 
454
- // Symbol (as atom)
453
+ // Symbol (as atom) - use token format (unquoted)
455
454
  if (typeof value === "symbol") {
456
455
  const name = Symbol.keyFor(value) || value.description || ""
457
- return ["atom", `"${name}"`]
456
+ return ["atom", name]
458
457
  }
459
458
 
460
459
  // List
package/esm/test.js CHANGED
@@ -1,6 +1,2 @@
1
- import { readFileSync } from "fs"
2
- import { resolve } from "path"
3
- import { acc } from "./accounts.js"
4
- import { toAddr, dirname, wait } from "./utils.js"
5
-
6
- export { toAddr, wait, acc }
1
+ import { toAddr } from "./utils.js"
2
+ export { toAddr }
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "hbsig",
3
- "version": "0.2.8",
3
+ "version": "0.3.1",
4
4
  "main": "cjs/index.js",
5
+ "module": "esm/index.js",
5
6
  "license": "MIT",
6
7
  "exports": {
7
8
  ".": {
@@ -28,7 +29,6 @@
28
29
  "ramda": "^0.31.3",
29
30
  "structured-headers": "1.0.1"
30
31
  },
31
- "module": "esm/index.js",
32
32
  "bin": {
33
33
  "wao": "./cjs/cli.js",
34
34
  "wao-esm": "./esm/cli.js"