@wovin/core 0.2.2 → 0.3.2

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 (112) hide show
  1. package/dist/applog/applog-utils.d.ts +25 -1
  2. package/dist/applog/applog-utils.d.ts.map +1 -1
  3. package/dist/applog/datom-types.d.ts.map +1 -1
  4. package/dist/applog.js +5 -3
  5. package/dist/blockstore.js +2 -0
  6. package/dist/blockstore.js.map +1 -1
  7. package/dist/{chunk-SHUHRHOT.js → chunk-4MKPGQIM.js} +17 -18
  8. package/dist/chunk-4MKPGQIM.js.map +1 -0
  9. package/dist/chunk-64EJIJAJ.js +17 -0
  10. package/dist/chunk-64EJIJAJ.js.map +1 -0
  11. package/dist/{chunk-HUIQ54TT.js → chunk-6CSJTSQP.js} +3 -3
  12. package/dist/chunk-7QEGHKR4.js +17 -0
  13. package/dist/chunk-7QEGHKR4.js.map +1 -0
  14. package/dist/{chunk-BLF5MAWU.js → chunk-BIYQEX3N.js} +2 -2
  15. package/dist/{chunk-6ALNRM3J.js → chunk-H3JNNTVP.js} +21 -8
  16. package/dist/chunk-H3JNNTVP.js.map +1 -0
  17. package/dist/{chunk-OC6Z6CQW.js → chunk-H4YVJKB7.js} +2 -2
  18. package/dist/chunk-ICBK7NC4.js +27 -0
  19. package/dist/chunk-ICBK7NC4.js.map +1 -0
  20. package/dist/chunk-N5QPZNKD.js +288 -0
  21. package/dist/chunk-N5QPZNKD.js.map +1 -0
  22. package/dist/{chunk-3SUFNJEZ.js → chunk-N7SEGHU4.js} +7 -2
  23. package/dist/{chunk-3SUFNJEZ.js.map → chunk-N7SEGHU4.js.map} +1 -1
  24. package/dist/chunk-XF4DWOAE.js +25 -0
  25. package/dist/chunk-XF4DWOAE.js.map +1 -0
  26. package/dist/index.js +20 -10
  27. package/dist/ipfs/car.d.ts.map +1 -1
  28. package/dist/ipfs.js +4 -4
  29. package/dist/ipns/gateway-resolver.d.ts +21 -0
  30. package/dist/ipns/gateway-resolver.d.ts.map +1 -0
  31. package/dist/ipns/ipns-record.d.ts +46 -7
  32. package/dist/ipns/ipns-record.d.ts.map +1 -1
  33. package/dist/ipns/ipns-w3name.d.ts +45 -0
  34. package/dist/ipns/ipns-w3name.d.ts.map +1 -0
  35. package/dist/ipns/ipns-watcher.d.ts +190 -0
  36. package/dist/ipns/ipns-watcher.d.ts.map +1 -0
  37. package/dist/ipns.d.ts +3 -0
  38. package/dist/ipns.d.ts.map +1 -1
  39. package/dist/ipns.js +591 -14
  40. package/dist/ipns.js.map +1 -1
  41. package/dist/pubsub/snap-push.d.ts +2 -2
  42. package/dist/pubsub/snap-push.d.ts.map +1 -1
  43. package/dist/pubsub.js +4 -4
  44. package/dist/query/epoch-snapshot.d.ts +205 -0
  45. package/dist/query/epoch-snapshot.d.ts.map +1 -0
  46. package/dist/query.d.ts +1 -0
  47. package/dist/query.d.ts.map +1 -1
  48. package/dist/query.js +12 -4
  49. package/dist/retrieve.js +4 -4
  50. package/dist/thread.js +1 -1
  51. package/dist/viewmodel/adapters/arktype.d.ts +33 -0
  52. package/dist/viewmodel/adapters/arktype.d.ts.map +1 -0
  53. package/dist/viewmodel/adapters/arktype.js +7 -0
  54. package/dist/viewmodel/adapters/arktype.js.map +1 -0
  55. package/dist/viewmodel/adapters/typebox.d.ts +35 -0
  56. package/dist/viewmodel/adapters/typebox.d.ts.map +1 -0
  57. package/dist/viewmodel/adapters/typebox.js +7 -0
  58. package/dist/viewmodel/adapters/typebox.js.map +1 -0
  59. package/dist/viewmodel/adapters/typia.d.ts +40 -0
  60. package/dist/viewmodel/adapters/typia.d.ts.map +1 -0
  61. package/dist/viewmodel/adapters/typia.js +7 -0
  62. package/dist/viewmodel/adapters/typia.js.map +1 -0
  63. package/dist/viewmodel/adapters/zod.d.ts +30 -0
  64. package/dist/viewmodel/adapters/zod.d.ts.map +1 -0
  65. package/dist/viewmodel/adapters/zod.js +7 -0
  66. package/dist/viewmodel/adapters/zod.js.map +1 -0
  67. package/dist/viewmodel/builder.d.ts +40 -0
  68. package/dist/viewmodel/builder.d.ts.map +1 -0
  69. package/dist/viewmodel/examples/all-adapters.d.ts +26 -0
  70. package/dist/viewmodel/examples/all-adapters.d.ts.map +1 -0
  71. package/dist/viewmodel/factory.d.ts +38 -0
  72. package/dist/viewmodel/factory.d.ts.map +1 -0
  73. package/dist/viewmodel/index.d.ts +10 -0
  74. package/dist/viewmodel/index.d.ts.map +1 -0
  75. package/dist/viewmodel/index.js +313 -0
  76. package/dist/viewmodel/index.js.map +1 -0
  77. package/dist/viewmodel/schema-adapter.d.ts +16 -0
  78. package/dist/viewmodel/schema-adapter.d.ts.map +1 -0
  79. package/dist/viewmodel/types.d.ts +97 -0
  80. package/dist/viewmodel/types.d.ts.map +1 -0
  81. package/package.json +29 -3
  82. package/src/applog/applog-utils.test.ts +43 -1
  83. package/src/applog/applog-utils.ts +38 -21
  84. package/src/applog/datom-types.ts +2 -2
  85. package/src/applog/object-values.test.ts +9 -9
  86. package/src/ipfs/car.ts +14 -2
  87. package/src/ipns/gateway-resolver.ts +63 -0
  88. package/src/ipns/ipns-record.ts +158 -29
  89. package/src/ipns/ipns-w3name.ts +207 -0
  90. package/src/ipns/ipns-watcher.ts +608 -0
  91. package/src/ipns.ts +3 -0
  92. package/src/pubsub/snap-push.ts +6 -5
  93. package/src/query/epoch-snapshot.test.ts +594 -0
  94. package/src/query/epoch-snapshot.ts +392 -0
  95. package/src/query.ts +1 -0
  96. package/src/viewmodel/adapters/arktype.ts +44 -0
  97. package/src/viewmodel/adapters/typebox.ts +59 -0
  98. package/src/viewmodel/adapters/typia.ts +50 -0
  99. package/src/viewmodel/adapters/zod.ts +55 -0
  100. package/src/viewmodel/builder.ts +71 -0
  101. package/src/viewmodel/examples/all-adapters.ts +206 -0
  102. package/src/viewmodel/factory.ts +330 -0
  103. package/src/viewmodel/index.ts +22 -0
  104. package/src/viewmodel/schema-adapter.ts +27 -0
  105. package/src/viewmodel/types.ts +152 -0
  106. package/dist/chunk-22WDFLXO.js +0 -138
  107. package/dist/chunk-22WDFLXO.js.map +0 -1
  108. package/dist/chunk-6ALNRM3J.js.map +0 -1
  109. package/dist/chunk-SHUHRHOT.js.map +0 -1
  110. /package/dist/{chunk-HUIQ54TT.js.map → chunk-6CSJTSQP.js.map} +0 -0
  111. /package/dist/{chunk-BLF5MAWU.js.map → chunk-BIYQEX3N.js.map} +0 -0
  112. /package/dist/{chunk-OC6Z6CQW.js.map → chunk-H4YVJKB7.js.map} +0 -0
@@ -0,0 +1,97 @@
1
+ import type { ApplogValue, EntityID } from '../applog/datom-types.ts';
2
+ import type { Thread } from '../thread/basic.ts';
3
+ import type { ObjectBuilder } from './builder.ts';
4
+ export interface VMAttributeDef {
5
+ /** Attribute name (short: e.g. 'name', 'content') */
6
+ name: string;
7
+ /** The wovin at-path used in applogs (e.g. 'block/content', 'speaker/name') */
8
+ atPath: string;
9
+ /** Whether the attribute is optional */
10
+ optional: boolean;
11
+ /** Default value if not present in applogs */
12
+ defaultValue?: ApplogValue;
13
+ }
14
+ /**
15
+ * A schema adapter converts a type-system schema (TypeBox, Zod, ArkType, Typia)
16
+ * into a uniform representation that the VM factory can consume.
17
+ */
18
+ export interface ISchemaAdapter<T extends Record<string, ApplogValue> = Record<string, ApplogValue>> {
19
+ /** Get the attribute definitions for this schema */
20
+ getAttributeDefs(): VMAttributeDef[];
21
+ /** Get default values for all attributes */
22
+ getDefaults(): Partial<T>;
23
+ /** Optional: runtime validator */
24
+ createValidator?(): (value: unknown) => value is T;
25
+ /** The entity prefix used for wovin at-paths (e.g. 'block', 'speaker') */
26
+ getEntityPrefix(): string;
27
+ }
28
+ export interface ViewModelFactoryOptions<T extends Record<string, ApplogValue>> {
29
+ /** The schema adapter for this entity type */
30
+ adapter: ISchemaAdapter<T>;
31
+ /** How to get the default thread context */
32
+ getDefaultThread?: () => Thread;
33
+ /** EntityID generation length (default: 7) */
34
+ entityIdLength?: number;
35
+ /** Function to generate entity IDs */
36
+ generateEntityId?: (data: Partial<T> & {
37
+ ts?: string;
38
+ }) => EntityID;
39
+ /** Override the entity prefix for at-paths */
40
+ entityPrefix?: string;
41
+ /** Optional function to make thread current-state (lastWriteWins) */
42
+ makeCurrentThread?: (thread: Thread) => Thread;
43
+ /** Name for the VM type (used in logging and debugging) */
44
+ vmName?: string;
45
+ /**
46
+ * Signal adapter for framework-specific reactivity.
47
+ * Default: non-reactive (direct Subscribable reads).
48
+ * For SolidJS, pass createSolidSignalAdapter() from @wovin/ui-solid/viewmodel.
49
+ */
50
+ signalAdapter?: ISignalAdapter;
51
+ }
52
+ export interface IVMInstance<T extends Record<string, ApplogValue> = Record<string, ApplogValue>> {
53
+ readonly en: EntityID;
54
+ readonly thread: Thread;
55
+ /** Get a builder for updating this entity */
56
+ buildUpdate(init?: Partial<T>): ObjectBuilder<T>;
57
+ /** Soft-delete this entity */
58
+ setDeleted(thread?: Thread): void;
59
+ /** Whether this entity is deleted */
60
+ readonly isDeleted: boolean;
61
+ /** The entity thread filtered to this entity */
62
+ readonly entityThread: Thread;
63
+ }
64
+ export interface IVMClass<T extends Record<string, ApplogValue> = Record<string, ApplogValue>> {
65
+ new (en: EntityID, thread: Thread, ...args: any[]): IVMInstance<T>;
66
+ /** Get or create a VM instance for the given entity ID and thread */
67
+ get(en: EntityID, thread: Thread): IVMInstance<T>;
68
+ /** Create a new entity builder */
69
+ buildNew(init?: Partial<T>, en?: EntityID): ObjectBuilder<T>;
70
+ }
71
+ /**
72
+ * A signal adapter abstracts how the VM creates reactive signals for each attribute.
73
+ * The core factory uses a default (non-reactive) implementation.
74
+ * Framework-specific extensions (Solid, Vue, etc.) override this.
75
+ */
76
+ export interface ISignalAdapter {
77
+ /**
78
+ * Create a reactive getter for a subscribable value.
79
+ * Returns a function that returns the current value.
80
+ * In the default implementation, this is just () => subscribable.value.
81
+ */
82
+ createGetter<T>(subscribable: {
83
+ value: T;
84
+ subscribe(cb: () => void): () => void;
85
+ }): () => T;
86
+ /**
87
+ * Create a writable signal for an attribute.
88
+ * Returns [get, set] pair.
89
+ * Default implementation stores and returns value directly.
90
+ */
91
+ createWritable<T>(initial: T): [() => T, (v: T) => void];
92
+ }
93
+ export type VMInstanceMap<T> = Map<EntityID, T>;
94
+ /** WeakMap from Thread → Map<VMName, Map<EntityID, VMInstance>> */
95
+ export declare const GlobalVMInstances: WeakMap<Thread, Map<string, Map<string, unknown>>>;
96
+ export declare function getInstancesForThread(thread: Thread): Map<string, Map<EntityID, unknown>>;
97
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/viewmodel/types.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wovin/core",
3
- "version": "0.2.2",
3
+ "version": "0.3.2",
4
4
  "type": "module",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.js",
@@ -57,6 +57,31 @@
57
57
  "import": "./dist/utils.js",
58
58
  "types": "./dist/utils.d.ts"
59
59
  },
60
+ "./viewmodel": {
61
+ "source": "./src/viewmodel/index.ts",
62
+ "import": "./dist/viewmodel/index.js",
63
+ "types": "./dist/viewmodel/index.d.ts"
64
+ },
65
+ "./viewmodel/adapters/typebox": {
66
+ "source": "./src/viewmodel/adapters/typebox.ts",
67
+ "import": "./dist/viewmodel/adapters/typebox.js",
68
+ "types": "./dist/viewmodel/adapters/typebox.d.ts"
69
+ },
70
+ "./viewmodel/adapters/zod": {
71
+ "source": "./src/viewmodel/adapters/zod.ts",
72
+ "import": "./dist/viewmodel/adapters/zod.js",
73
+ "types": "./dist/viewmodel/adapters/zod.d.ts"
74
+ },
75
+ "./viewmodel/adapters/arktype": {
76
+ "source": "./src/viewmodel/adapters/arktype.ts",
77
+ "import": "./dist/viewmodel/adapters/arktype.js",
78
+ "types": "./dist/viewmodel/adapters/arktype.d.ts"
79
+ },
80
+ "./viewmodel/adapters/typia": {
81
+ "source": "./src/viewmodel/adapters/typia.ts",
82
+ "import": "./dist/viewmodel/adapters/typia.js",
83
+ "types": "./dist/viewmodel/adapters/typia.d.ts"
84
+ },
60
85
  "./ipns": {
61
86
  "source": "./src/ipns.ts",
62
87
  "import": "./dist/ipns.js",
@@ -67,7 +92,6 @@
67
92
  "./dist/",
68
93
  "./src/"
69
94
  ],
70
- "packageManager": "pnpm",
71
95
  "esm.sh": {
72
96
  "bundle": false
73
97
  },
@@ -88,8 +112,10 @@
88
112
  "lodash-es": "^4.17.21",
89
113
  "@libp2p/crypto": "^5.0.0",
90
114
  "ipns": "^10.1.2",
91
- "multiformats": "^13.4.1",
115
+ "multiformats": "^14.0.0",
116
+ "partysocket": "^1.0.2",
92
117
  "safe-stable-stringify": "^2.5.0",
118
+ "w3name": "^1.1.3",
93
119
  "zod": "^4.3.6"
94
120
  },
95
121
  "devDependencies": {
@@ -1,6 +1,6 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import type { Applog } from './datom-types.ts'
3
- import { compareApplogsByTs, isLaterByTsAndPv, sortApplogsByTs } from './applog-utils.ts'
3
+ import { compareApplogsByTs, deriveDeterministicEntityID, isLaterByTsAndPv, mintNewEntity, sortApplogsByTs } from './applog-utils.ts'
4
4
 
5
5
  const mkLog = (overrides: Partial<Applog>): Applog => ({
6
6
  cid: 'cid-default' as any,
@@ -106,3 +106,45 @@ describe('isLaterByTsAndPv', () => {
106
106
  expect(isLaterByTsAndPv(b, a)).toBe('other'.localeCompare('shared') > 0)
107
107
  })
108
108
  })
109
+
110
+ describe('deriveDeterministicEntityID — natural-key identity', () => {
111
+ it('is deterministic: same key ⇒ same id (write & read converge on one soul)', () => {
112
+ const a = deriveDeterministicEntityID({ type: 'segment', mediaId: 'm1', startMs: 0, endMs: 100 })
113
+ const b = deriveDeterministicEntityID({ type: 'segment', mediaId: 'm1', startMs: 0, endMs: 100 })
114
+ expect(a).toBe(b)
115
+ })
116
+
117
+ it('is stable across key property order (safe-stable-stringify)', () => {
118
+ const a = deriveDeterministicEntityID({ mediaId: 'm1', type: 'segment' })
119
+ const b = deriveDeterministicEntityID({ type: 'segment', mediaId: 'm1' })
120
+ expect(a).toBe(b)
121
+ })
122
+
123
+ it('different keys ⇒ different ids', () => {
124
+ expect(deriveDeterministicEntityID('#rust')).not.toBe(deriveDeterministicEntityID('#typescript'))
125
+ })
126
+
127
+ it('honors the requested length', () => {
128
+ expect(deriveDeterministicEntityID('#rust', 7)).toHaveLength(7)
129
+ expect(deriveDeterministicEntityID('#rust', 10)).toHaveLength(10)
130
+ })
131
+ })
132
+
133
+ describe('mintNewEntity — occurrence identity', () => {
134
+ it('re-minting the same logical seed with a fresh ts yields a DIFFERENT id (the re-mint trap)', () => {
135
+ // Documents why an existing entity must be resolved & its `en` reused rather than
136
+ // re-minted: a timestamp in the seed forks a new soul on every call.
137
+ const first = mintNewEntity({ msgId: 42, ts: '2026-06-16T00:00:00.000Z' })
138
+ const editedLater = mintNewEntity({ msgId: 42, ts: '2026-06-16T00:00:01.000Z' })
139
+ expect(first).not.toBe(editedLater)
140
+ })
141
+
142
+ it('is deterministic for an identical seed (same ts ⇒ same id)', () => {
143
+ const seed = { msgId: 42, ts: '2026-06-16T00:00:00.000Z' }
144
+ expect(mintNewEntity(seed)).toBe(mintNewEntity({ ...seed }))
145
+ })
146
+
147
+ it('honors the requested length', () => {
148
+ expect(mintNewEntity({ a: 1 }, 6)).toHaveLength(6)
149
+ })
150
+ })
@@ -255,7 +255,35 @@ export const removeDuplicateAppLogs = (appLogArray: Applog[], mode?: RemoveDupli
255
255
  // return Array.from(logMap.values())
256
256
  // }
257
257
 
258
- export const getHashID = (stringifiable: any, lngth = 8) => cyrb53hash(stringify(stringifiable), 31, lngth) as string
258
+ /** Low-level: stable-stringify + cyrb53 hex entity id of `lngth` chars. Prefer the named wrappers below. */
259
+ const hashToEntityId = (stringifiable: any, lngth = 8) => cyrb53hash(stringify(stringifiable), 31, lngth) as string
260
+
261
+ /**
262
+ * Mint a brand-new entity id — coin a fresh "soul" for a genuinely new occurrence.
263
+ *
264
+ * The hashed `seed` MUST include something that makes this occurrence unique (a timestamp
265
+ * and/or random nonce), otherwise two distinct creations collide on one id.
266
+ *
267
+ * ⚠️ NEVER call this to re-reference an entity that already exists: doing so mints a *new*
268
+ * soul and forks a duplicate (breaking pv / lastWriteWins chains). To reference an existing
269
+ * entity, look it up and reuse its `en`. For identity that is *defined by* a stable key, use
270
+ * {@link deriveDeterministicEntityID}. See docs/architecture.md#entity-identity-soul.
271
+ */
272
+ export const mintNewEntity = (seed: any, lngth = 8) => hashToEntityId(seed, lngth)
273
+
274
+ /**
275
+ * Derive a deterministic entity id from a stable, identity-defining key.
276
+ *
277
+ * Same key ⇒ same `en`, so all references converge on one soul — used for both writing and
278
+ * reading the same entity (singleton / natural-key entities, e.g. a tag, a per-agent setting,
279
+ * a media segment).
280
+ *
281
+ * ⚠️ Use with care: two users/agents using the same name/key for something does NOT necessarily
282
+ * mean they mean the same thing/entity/soul. Only use when the key TRULY defines identity. The
283
+ * key must be canonicalized — diverging formats fork the soul and silently break lookups.
284
+ * See docs/architecture.md#entity-identity-soul.
285
+ */
286
+ export const deriveDeterministicEntityID = (stableKey: any, lngth = 8) => hashToEntityId(stableKey, lngth)
259
287
 
260
288
  export function isVariable(x: any): x is string {
261
289
  return typeof x === 'string' && x.startsWith('?')
@@ -324,21 +352,16 @@ export function matchPartStatic(field: keyof Applog, patternPart: ValueOrMatcher
324
352
  result = patternPart === atomPart // shortcut for most common use-case
325
353
  } else if (typ === 'function') {
326
354
  result = (patternPart as Function)(atomPart)
327
- } else if (Array.isArray(patternPart)) {
328
- // A bare array is ambiguous now that `vl` may itself be an array value: it could
329
- // mean set-membership or a literal-array match. Fail loudly rather than match wrong.
330
- throw ERROR(
331
- `[matchPartStatic] a bare array is not a valid matcher for field '${field}'.`
332
- + ` Use anyOf(...) for set-membership, or a predicate (v) => isEqual(v, [...]) to match a literal array value.`,
333
- patternPart,
334
- )
335
- } else if (typeof (patternPart as any).has === 'function') {
336
- result = (patternPart as Set<any>).has(atomPart)
355
+ } else if (patternPart instanceof Set) {
356
+ // A Set means set-membership (use anyOf(...) to build one). Bare arrays/objects are
357
+ // NOT membership they are literal values compared by deep equality (see else).
358
+ result = patternPart.has(atomPart)
337
359
  } // if (field === 'at' && typ === 'string' && patternPart.endsWith('*')) {
338
360
  // return typeof atomPart === 'string' && atomPart.startsWith(patternPart.slice(0, -1))
339
361
  // }
340
362
  else {
341
- // object/array values compare by deep equality; primitives via isEqual's === fast-path
363
+ // object/array values (and primitives) compare by deep equality;
364
+ // isEqual short-circuits on === so primitives stay on the cheap path
342
365
  result = valueEq(patternPart as ApplogValue, atomPart)
343
366
  }
344
367
  } else {
@@ -368,15 +391,9 @@ export function matchPart(patternPart: ValueOrMatcher<ApplogValue>, atomPart: Ap
368
391
  if (typeof patternPart === 'function') {
369
392
  return patternPart(atomPart) ? context : null
370
393
  }
371
- if (Array.isArray(patternPart)) {
372
- throw ERROR(
373
- `[matchPart] a bare array is not a valid matcher.`
374
- + ` Use anyOf(...) for set-membership, or a predicate to match a literal array value.`,
375
- patternPart,
376
- )
377
- }
378
- if (patternPart && typeof (patternPart as any).has === 'function') {
379
- return (patternPart as Set<any>).has(atomPart) ? context : null
394
+ if (patternPart instanceof Set) {
395
+ // set-membership (use anyOf(...)); bare arrays/objects are literal deep-equality matches below
396
+ return patternPart.has(atomPart) ? context : null
380
397
  }
381
398
  return valueEq(patternPart as ApplogValue, atomPart) ? context : null
382
399
  }
@@ -118,12 +118,12 @@ type MapKeysStripPrefix<SELECT extends string, TPrefix extends string> = {
118
118
  }
119
119
 
120
120
  FormatRegistry.Set('CID', (value) => !!value.match(isCID))
121
- export const CIDTB = Type.String({ format: 'CID' })
121
+ export const CIDTB = Type.String({ pattern: isCID.source })
122
122
  export type CIDTB = Static<typeof EntityID>
123
123
 
124
124
  const isURL = /^http([s]?):\/\/.*\..*/
125
125
  FormatRegistry.Set('URL', (value) => !!value.match(isURL))
126
- export const URL = Type.String({ format: 'URL' })
126
+ export const URL = Type.String({ pattern: isURL.source })
127
127
  export type URL = Static<typeof URL>
128
128
 
129
129
  // Recursive JSON value: primitives plus nested arrays/objects (mirrors ApplogValue / JsonValue).
@@ -74,15 +74,12 @@ describe('object/array applog values', () => {
74
74
  expect(index.get(valueKey({ a: 9 }))).toHaveLength(1)
75
75
  })
76
76
 
77
- it('matchPartStatic matches a literal object value by deep equality', () => {
77
+ it('matchPartStatic matches a literal object/array value by deep equality', () => {
78
78
  expect(matchPartStatic('vl', { a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true)
79
79
  expect(matchPartStatic('vl', { a: 1 }, { a: 2 })).toBe(false)
80
- })
81
-
82
- it('matching a literal array value requires a predicate (a bare array throws)', () => {
83
- // bare arrays are reserved/rejected, so an array value is matched via a predicate
84
- expect(matchPartStatic('vl', (v: any) => valueEq(v, [1, 2, 3]), [1, 2, 3])).toBe(true)
85
- expect(() => matchPartStatic('vl', [1, 2, 3] as any, [1, 2, 3])).toThrow(/anyOf/)
80
+ // a bare array is a literal value, matched deeply (NOT set-membership)
81
+ expect(matchPartStatic('vl', [1, 2, 3], [1, 2, 3])).toBe(true)
82
+ expect(matchPartStatic('vl', [1, 2], [1, 2, 3])).toBe(false)
86
83
  })
87
84
 
88
85
  it('anyOf(...) provides set-membership and matches via matchPartStatic', () => {
@@ -96,8 +93,11 @@ describe('object/array applog values', () => {
96
93
  expect(matchPartStatic('vl', isDark, { theme: 'light' })).toBe(false)
97
94
  })
98
95
 
99
- it('a bare array pattern fails loudly rather than matching ambiguously', () => {
100
- expect(() => matchPartStatic('at', ['a', 'b'] as any, 'a')).toThrow(/anyOf/)
96
+ it('a bare array is a literal value, not set-membership (use anyOf for membership)', () => {
97
+ // ['a','b'] is the literal value to match, so it does NOT match the string 'a'
98
+ expect(matchPartStatic('at', ['a', 'b'], 'a')).toBe(false)
99
+ // membership is explicit via anyOf
100
+ expect(matchPartStatic('at', anyOf('a', 'b'), 'a')).toBe(true)
101
101
  })
102
102
 
103
103
  it('anyOf rejects object/array members (would silently never match)', () => {
package/src/ipfs/car.ts CHANGED
@@ -69,11 +69,23 @@ export async function decodePubFromBlocks(
69
69
  // New(er) format
70
70
  if (!applogsCID) applogsCID = root.applogs // Save from first snapshot
71
71
  const applogsBlock = (await getDecodedBlock(blockStore, root.applogs)) as SnapBlockLogsOrChunks
72
+ if (!applogsBlock) {
73
+ throw ERROR('[decodePubFromBlocks] applogs block missing from blockStore', {
74
+ cid: root.applogs.toString(),
75
+ rootCID: cidStr,
76
+ })
77
+ }
72
78
  pubLogsArray = await unchunkApplogsBlock(applogsBlock, blockStore)
73
79
  // Info only from first (most recent) snapshot
74
80
  if (!firstInfo) {
75
- firstInfo = (await getDecodedBlock(blockStore, root.info)) as SnapBlockLogs
76
- DEBUG(`new format - infoLogs`, firstInfo.logs.map(l => ({ [l.toString()]: l })))
81
+ const decoded = (await getDecodedBlock(blockStore, root.info)) as SnapBlockLogs
82
+ if (decoded) {
83
+ firstInfo = decoded
84
+ DEBUG(`new format - infoLogs`, firstInfo.logs.map(l => ({ [l.toString()]: l })))
85
+ } else {
86
+ WARN(`[decodePubFromBlocks] info block not found for ${root.info}, using empty info`)
87
+ firstInfo = { logs: [] }
88
+ }
77
89
  }
78
90
  // TODO: verify signatures
79
91
  } else {
@@ -0,0 +1,63 @@
1
+ import { CID } from 'multiformats/cid'
2
+ import { Logger } from 'besonders-logger'
3
+
4
+ const { WARN, LOG, DEBUG } = Logger.setup(Logger.INFO) // eslint-disable-line unused-imports/no-unused-vars
5
+
6
+ /**
7
+ * Resolve an IPNS name to a CID using a public IPFS gateway's HTTP HEAD response.
8
+ *
9
+ * Mechanism: per the IPFS HTTP Gateway spec, `HEAD <gateway>/ipns/<name>/` returns the
10
+ * IPNS-resolved root CID in the `X-Ipfs-Roots` response header (space-separated, ordered
11
+ * from root to leaf). The first entry is the IPNS-resolved CID.
12
+ *
13
+ * This works against any gateway that follows the spec and exposes CORS for HEAD
14
+ * (most public gateways do, e.g. ipfs.zt.ax, ipfs.io, dweb.link).
15
+ *
16
+ * The legacy w3name HTTP endpoint (`GET <base>/name/<ipns>` returning `{value: "/ipfs/<cid>"}`)
17
+ * is also supported here for back-compat with self-hosted w3name-like services — but the
18
+ * X-Ipfs-Roots path is preferred since it's the standardised gateway mechanism.
19
+ *
20
+ * @param ipns - The IPNS name (k51... string)
21
+ * @param gateways - List of gateway base URLs (e.g. `["https://ipfs.zt.ax"]`)
22
+ * @returns The resolved CID, or null if no gateway could resolve the name
23
+ */
24
+ export async function resolveIPNSViaGateway(ipns: string, gateways: string[]): Promise<CID | null> {
25
+ if (!ipns.startsWith('k51')) return null // only IPNS libp2p-key names go through gateway
26
+ if (!gateways?.length) return null
27
+
28
+ for (const rawGateway of gateways) {
29
+ const gateway = rawGateway.replace(/\/+$/, '')
30
+ const url = `${gateway}/ipns/${ipns}/`
31
+ try {
32
+ DEBUG(`[resolveIPNSViaGateway] HEAD ${url}`)
33
+ const response = await fetch(url, { method: 'HEAD' })
34
+ if (!response.ok) {
35
+ DEBUG(`[resolveIPNSViaGateway] ${gateway} returned ${response.status}`)
36
+ continue
37
+ }
38
+ const roots = response.headers.get('x-ipfs-roots') ?? response.headers.get('X-Ipfs-Roots')
39
+ if (roots) {
40
+ const first = roots.split(/[\s,]+/)[0]?.trim()
41
+ if (first) {
42
+ const cid = CID.parse(first)
43
+ DEBUG(`[resolveIPNSViaGateway] resolved via ${gateway} x-ipfs-roots:`, cid.toString())
44
+ return cid
45
+ }
46
+ }
47
+ // Some gateways omit X-Ipfs-Roots but put the CID in etag (e.g. `<cid>.dag-json`)
48
+ const etag = response.headers.get('etag')
49
+ if (etag) {
50
+ const m = etag.match(/^"?([a-z0-9]+)/i)
51
+ if (m) {
52
+ const cid = CID.parse(m[1])
53
+ DEBUG(`[resolveIPNSViaGateway] resolved via ${gateway} etag:`, cid.toString())
54
+ return cid
55
+ }
56
+ }
57
+ WARN(`[resolveIPNSViaGateway] ${gateway} returned 200 but no usable CID header`)
58
+ } catch (err) {
59
+ WARN(`[resolveIPNSViaGateway] ${gateway} failed:`, err)
60
+ }
61
+ }
62
+ return null
63
+ }