@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.
- package/dist/applog/applog-utils.d.ts +25 -1
- package/dist/applog/applog-utils.d.ts.map +1 -1
- package/dist/applog/datom-types.d.ts.map +1 -1
- package/dist/applog.js +5 -3
- package/dist/blockstore.js +2 -0
- package/dist/blockstore.js.map +1 -1
- package/dist/{chunk-SHUHRHOT.js → chunk-4MKPGQIM.js} +17 -18
- package/dist/chunk-4MKPGQIM.js.map +1 -0
- package/dist/chunk-64EJIJAJ.js +17 -0
- package/dist/chunk-64EJIJAJ.js.map +1 -0
- package/dist/{chunk-HUIQ54TT.js → chunk-6CSJTSQP.js} +3 -3
- package/dist/chunk-7QEGHKR4.js +17 -0
- package/dist/chunk-7QEGHKR4.js.map +1 -0
- package/dist/{chunk-BLF5MAWU.js → chunk-BIYQEX3N.js} +2 -2
- package/dist/{chunk-6ALNRM3J.js → chunk-H3JNNTVP.js} +21 -8
- package/dist/chunk-H3JNNTVP.js.map +1 -0
- package/dist/{chunk-OC6Z6CQW.js → chunk-H4YVJKB7.js} +2 -2
- package/dist/chunk-ICBK7NC4.js +27 -0
- package/dist/chunk-ICBK7NC4.js.map +1 -0
- package/dist/chunk-N5QPZNKD.js +288 -0
- package/dist/chunk-N5QPZNKD.js.map +1 -0
- package/dist/{chunk-3SUFNJEZ.js → chunk-N7SEGHU4.js} +7 -2
- package/dist/{chunk-3SUFNJEZ.js.map → chunk-N7SEGHU4.js.map} +1 -1
- package/dist/chunk-XF4DWOAE.js +25 -0
- package/dist/chunk-XF4DWOAE.js.map +1 -0
- package/dist/index.js +20 -10
- package/dist/ipfs/car.d.ts.map +1 -1
- package/dist/ipfs.js +4 -4
- package/dist/ipns/gateway-resolver.d.ts +21 -0
- package/dist/ipns/gateway-resolver.d.ts.map +1 -0
- package/dist/ipns/ipns-record.d.ts +46 -7
- package/dist/ipns/ipns-record.d.ts.map +1 -1
- package/dist/ipns/ipns-w3name.d.ts +45 -0
- package/dist/ipns/ipns-w3name.d.ts.map +1 -0
- package/dist/ipns/ipns-watcher.d.ts +190 -0
- package/dist/ipns/ipns-watcher.d.ts.map +1 -0
- package/dist/ipns.d.ts +3 -0
- package/dist/ipns.d.ts.map +1 -1
- package/dist/ipns.js +591 -14
- package/dist/ipns.js.map +1 -1
- package/dist/pubsub/snap-push.d.ts +2 -2
- package/dist/pubsub/snap-push.d.ts.map +1 -1
- package/dist/pubsub.js +4 -4
- package/dist/query/epoch-snapshot.d.ts +205 -0
- package/dist/query/epoch-snapshot.d.ts.map +1 -0
- package/dist/query.d.ts +1 -0
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +12 -4
- package/dist/retrieve.js +4 -4
- package/dist/thread.js +1 -1
- package/dist/viewmodel/adapters/arktype.d.ts +33 -0
- package/dist/viewmodel/adapters/arktype.d.ts.map +1 -0
- package/dist/viewmodel/adapters/arktype.js +7 -0
- package/dist/viewmodel/adapters/arktype.js.map +1 -0
- package/dist/viewmodel/adapters/typebox.d.ts +35 -0
- package/dist/viewmodel/adapters/typebox.d.ts.map +1 -0
- package/dist/viewmodel/adapters/typebox.js +7 -0
- package/dist/viewmodel/adapters/typebox.js.map +1 -0
- package/dist/viewmodel/adapters/typia.d.ts +40 -0
- package/dist/viewmodel/adapters/typia.d.ts.map +1 -0
- package/dist/viewmodel/adapters/typia.js +7 -0
- package/dist/viewmodel/adapters/typia.js.map +1 -0
- package/dist/viewmodel/adapters/zod.d.ts +30 -0
- package/dist/viewmodel/adapters/zod.d.ts.map +1 -0
- package/dist/viewmodel/adapters/zod.js +7 -0
- package/dist/viewmodel/adapters/zod.js.map +1 -0
- package/dist/viewmodel/builder.d.ts +40 -0
- package/dist/viewmodel/builder.d.ts.map +1 -0
- package/dist/viewmodel/examples/all-adapters.d.ts +26 -0
- package/dist/viewmodel/examples/all-adapters.d.ts.map +1 -0
- package/dist/viewmodel/factory.d.ts +38 -0
- package/dist/viewmodel/factory.d.ts.map +1 -0
- package/dist/viewmodel/index.d.ts +10 -0
- package/dist/viewmodel/index.d.ts.map +1 -0
- package/dist/viewmodel/index.js +313 -0
- package/dist/viewmodel/index.js.map +1 -0
- package/dist/viewmodel/schema-adapter.d.ts +16 -0
- package/dist/viewmodel/schema-adapter.d.ts.map +1 -0
- package/dist/viewmodel/types.d.ts +97 -0
- package/dist/viewmodel/types.d.ts.map +1 -0
- package/package.json +29 -3
- package/src/applog/applog-utils.test.ts +43 -1
- package/src/applog/applog-utils.ts +38 -21
- package/src/applog/datom-types.ts +2 -2
- package/src/applog/object-values.test.ts +9 -9
- package/src/ipfs/car.ts +14 -2
- package/src/ipns/gateway-resolver.ts +63 -0
- package/src/ipns/ipns-record.ts +158 -29
- package/src/ipns/ipns-w3name.ts +207 -0
- package/src/ipns/ipns-watcher.ts +608 -0
- package/src/ipns.ts +3 -0
- package/src/pubsub/snap-push.ts +6 -5
- package/src/query/epoch-snapshot.test.ts +594 -0
- package/src/query/epoch-snapshot.ts +392 -0
- package/src/query.ts +1 -0
- package/src/viewmodel/adapters/arktype.ts +44 -0
- package/src/viewmodel/adapters/typebox.ts +59 -0
- package/src/viewmodel/adapters/typia.ts +50 -0
- package/src/viewmodel/adapters/zod.ts +55 -0
- package/src/viewmodel/builder.ts +71 -0
- package/src/viewmodel/examples/all-adapters.ts +206 -0
- package/src/viewmodel/factory.ts +330 -0
- package/src/viewmodel/index.ts +22 -0
- package/src/viewmodel/schema-adapter.ts +27 -0
- package/src/viewmodel/types.ts +152 -0
- package/dist/chunk-22WDFLXO.js +0 -138
- package/dist/chunk-22WDFLXO.js.map +0 -1
- package/dist/chunk-6ALNRM3J.js.map +0 -1
- package/dist/chunk-SHUHRHOT.js.map +0 -1
- /package/dist/{chunk-HUIQ54TT.js.map → chunk-6CSJTSQP.js.map} +0 -0
- /package/dist/{chunk-BLF5MAWU.js.map → chunk-BIYQEX3N.js.map} +0 -0
- /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.
|
|
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": "^
|
|
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
|
-
|
|
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 (
|
|
328
|
-
// A
|
|
329
|
-
//
|
|
330
|
-
|
|
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;
|
|
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 (
|
|
372
|
-
|
|
373
|
-
|
|
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({
|
|
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({
|
|
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
|
-
|
|
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
|
|
100
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
+
}
|