@wovin/core 0.2.2 → 0.3.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.
- package/dist/applog/datom-types.d.ts.map +1 -1
- package/dist/applog.js +1 -1
- package/dist/blockstore.js +2 -0
- package/dist/blockstore.js.map +1 -1
- package/dist/{chunk-SHUHRHOT.js → chunk-2OXLPZQI.js} +10 -3
- package/dist/chunk-2OXLPZQI.js.map +1 -0
- package/dist/{chunk-3SUFNJEZ.js → chunk-2PJFLZRC.js} +7 -2
- package/dist/{chunk-3SUFNJEZ.js.map → chunk-2PJFLZRC.js.map} +1 -1
- package/dist/chunk-64EJIJAJ.js +17 -0
- package/dist/chunk-64EJIJAJ.js.map +1 -0
- package/dist/chunk-7QEGHKR4.js +17 -0
- package/dist/chunk-7QEGHKR4.js.map +1 -0
- package/dist/{chunk-OC6Z6CQW.js → chunk-EHO2BFFY.js} +2 -2
- package/dist/chunk-ICBK7NC4.js +27 -0
- package/dist/chunk-ICBK7NC4.js.map +1 -0
- package/dist/{chunk-22WDFLXO.js → chunk-OKXRRWNS.js} +3 -3
- package/dist/{chunk-6ALNRM3J.js → chunk-Q4EMPWA3.js} +15 -8
- package/dist/chunk-Q4EMPWA3.js.map +1 -0
- package/dist/{chunk-HUIQ54TT.js → chunk-VGIACGWX.js} +3 -3
- package/dist/{chunk-BLF5MAWU.js → chunk-WVW4YXB5.js} +2 -2
- package/dist/chunk-XF4DWOAE.js +25 -0
- package/dist/chunk-XF4DWOAE.js.map +1 -0
- package/dist/index.js +7 -7
- 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 +28 -7
- package/dist/ipns/ipns-record.d.ts.map +1 -1
- package/dist/ipns/ipns-w3name.d.ts +15 -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 +488 -8
- 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.js +3 -3
- 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/datom-types.ts +2 -2
- package/src/ipfs/car.ts +8 -2
- package/src/ipns/gateway-resolver.ts +63 -0
- package/src/ipns/ipns-record.ts +68 -17
- package/src/ipns/ipns-w3name.ts +103 -0
- package/src/ipns/ipns-watcher.ts +607 -0
- package/src/ipns.ts +3 -0
- package/src/pubsub/snap-push.ts +6 -5
- 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-6ALNRM3J.js.map +0 -1
- package/dist/chunk-SHUHRHOT.js.map +0 -1
- /package/dist/{chunk-OC6Z6CQW.js.map → chunk-EHO2BFFY.js.map} +0 -0
- /package/dist/{chunk-22WDFLXO.js.map → chunk-OKXRRWNS.js.map} +0 -0
- /package/dist/{chunk-HUIQ54TT.js.map → chunk-VGIACGWX.js.map} +0 -0
- /package/dist/{chunk-BLF5MAWU.js.map → chunk-WVW4YXB5.js.map} +0 -0
|
@@ -0,0 +1,313 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createArkTypeAdapter
|
|
3
|
+
} from "../chunk-64EJIJAJ.js";
|
|
4
|
+
import {
|
|
5
|
+
createTypeBoxAdapter
|
|
6
|
+
} from "../chunk-ICBK7NC4.js";
|
|
7
|
+
import {
|
|
8
|
+
createTypiaAdapter
|
|
9
|
+
} from "../chunk-7QEGHKR4.js";
|
|
10
|
+
import {
|
|
11
|
+
createZodAdapter
|
|
12
|
+
} from "../chunk-XF4DWOAE.js";
|
|
13
|
+
import {
|
|
14
|
+
lastWriteWins,
|
|
15
|
+
liveEntityAt
|
|
16
|
+
} from "../chunk-2PJFLZRC.js";
|
|
17
|
+
import {
|
|
18
|
+
EntityID_LENGTH,
|
|
19
|
+
SubscribableImpl,
|
|
20
|
+
dateNowIso,
|
|
21
|
+
ensureTsPvAndFinalizeApplog,
|
|
22
|
+
getHashID,
|
|
23
|
+
isInitEvent,
|
|
24
|
+
rollingFilter
|
|
25
|
+
} from "../chunk-2OXLPZQI.js";
|
|
26
|
+
import "../chunk-ZAADLBSB.js";
|
|
27
|
+
|
|
28
|
+
// src/viewmodel/builder.ts
|
|
29
|
+
var ObjectBuilder = class _ObjectBuilder {
|
|
30
|
+
constructor(_data, en = null, _atPrefix = null, _atOverrides = {}) {
|
|
31
|
+
this._data = _data;
|
|
32
|
+
this.en = en;
|
|
33
|
+
this._atPrefix = _atPrefix;
|
|
34
|
+
this._atOverrides = _atOverrides;
|
|
35
|
+
}
|
|
36
|
+
_data;
|
|
37
|
+
en;
|
|
38
|
+
_atPrefix;
|
|
39
|
+
_atOverrides;
|
|
40
|
+
/**
|
|
41
|
+
* Create a new builder with initial data.
|
|
42
|
+
*/
|
|
43
|
+
static create(init = {}, en, atPrefix, atOverrides) {
|
|
44
|
+
return new _ObjectBuilder(init, en, atPrefix, atOverrides);
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Update the data being built.
|
|
48
|
+
*/
|
|
49
|
+
update(updateObj) {
|
|
50
|
+
Object.assign(this._data, updateObj);
|
|
51
|
+
return this;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the applogs for this entity, optionally resolving against a thread.
|
|
55
|
+
*/
|
|
56
|
+
build(thread) {
|
|
57
|
+
const atPrefix = this._atPrefix;
|
|
58
|
+
const applogs = [];
|
|
59
|
+
for (const [key, value] of Object.entries(this._data)) {
|
|
60
|
+
if (value === void 0) continue;
|
|
61
|
+
const atOverride = this._atOverrides[key];
|
|
62
|
+
const at = atOverride ?? (atPrefix ? `${atPrefix}/${String(key)}` : String(key));
|
|
63
|
+
applogs.push({ en: this.en, at, vl: value });
|
|
64
|
+
}
|
|
65
|
+
return applogs;
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Get the raw data being built.
|
|
69
|
+
*/
|
|
70
|
+
get data() {
|
|
71
|
+
return { ...this._data };
|
|
72
|
+
}
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
// src/viewmodel/types.ts
|
|
76
|
+
var GlobalVMInstances = /* @__PURE__ */ new WeakMap();
|
|
77
|
+
function getInstancesForThread(thread) {
|
|
78
|
+
let threadMap = GlobalVMInstances.get(thread);
|
|
79
|
+
if (!threadMap) {
|
|
80
|
+
threadMap = /* @__PURE__ */ new Map();
|
|
81
|
+
GlobalVMInstances.set(thread, threadMap);
|
|
82
|
+
}
|
|
83
|
+
return threadMap;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/viewmodel/factory.ts
|
|
87
|
+
var DefaultSignalAdapter = {
|
|
88
|
+
createGetter(subscribable) {
|
|
89
|
+
subscribable.subscribe(() => {
|
|
90
|
+
});
|
|
91
|
+
return () => subscribable.value;
|
|
92
|
+
},
|
|
93
|
+
createWritable(initial) {
|
|
94
|
+
let current = initial;
|
|
95
|
+
return [() => current, (v) => {
|
|
96
|
+
current = v;
|
|
97
|
+
}];
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
function makeCurrent(thread, fn) {
|
|
101
|
+
if (fn) return fn(thread);
|
|
102
|
+
return lastWriteWins(thread, { tolerateAlreadyFiltered: true });
|
|
103
|
+
}
|
|
104
|
+
function defaultPersistApplogs(thread, applogs) {
|
|
105
|
+
const finalized = applogs.map((log) => ensureTsPvAndFinalizeApplog(log, thread));
|
|
106
|
+
const writable = thread;
|
|
107
|
+
if (typeof writable.insertRaw === "function") {
|
|
108
|
+
writable.insertRaw(finalized);
|
|
109
|
+
} else if (typeof writable.insert === "function") {
|
|
110
|
+
writable.insert(applogs);
|
|
111
|
+
} else {
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function createViewModelFactory(options) {
|
|
115
|
+
const {
|
|
116
|
+
adapter,
|
|
117
|
+
entityPrefix = adapter.getEntityPrefix(),
|
|
118
|
+
entityIdLength = EntityID_LENGTH,
|
|
119
|
+
generateEntityId,
|
|
120
|
+
vmName = entityPrefix,
|
|
121
|
+
signalAdapter = DefaultSignalAdapter
|
|
122
|
+
} = options;
|
|
123
|
+
const attrDefs = adapter.getAttributeDefs();
|
|
124
|
+
const defaults = adapter.getDefaults();
|
|
125
|
+
const genId = generateEntityId ?? ((data) => getHashID({ ...data, ts: data.ts ?? dateNowIso() }, entityIdLength));
|
|
126
|
+
const persistFn = (thread, applogs) => {
|
|
127
|
+
defaultPersistApplogs(thread, applogs);
|
|
128
|
+
};
|
|
129
|
+
class ViewModel {
|
|
130
|
+
constructor(en, thread, skipInit) {
|
|
131
|
+
this.en = en;
|
|
132
|
+
this.thread = thread;
|
|
133
|
+
if (skipInit) return;
|
|
134
|
+
const currentThread = makeCurrent(thread, options.makeCurrentThread);
|
|
135
|
+
for (const attr of attrDefs) {
|
|
136
|
+
if (attr.name === "en") continue;
|
|
137
|
+
const attrName = attr.name;
|
|
138
|
+
const atPath = attr.atPath;
|
|
139
|
+
const defaultValue = attr.defaultValue ?? defaults?.[attrName];
|
|
140
|
+
Object.defineProperty(this, attrName, {
|
|
141
|
+
get() {
|
|
142
|
+
let signal = this._signals.get(attrName);
|
|
143
|
+
if (!signal) {
|
|
144
|
+
const subscribable = liveEntityAt(currentThread, this.en, atPath);
|
|
145
|
+
signal = this._signalAdapter.createGetter(subscribable);
|
|
146
|
+
this._signals.set(attrName, signal);
|
|
147
|
+
}
|
|
148
|
+
const value = signal();
|
|
149
|
+
return value ?? defaultValue ?? value;
|
|
150
|
+
},
|
|
151
|
+
set(v) {
|
|
152
|
+
const applog = { en: this.en, at: atPath, vl: v };
|
|
153
|
+
persistFn(this.thread, [applog]);
|
|
154
|
+
},
|
|
155
|
+
enumerable: true,
|
|
156
|
+
configurable: true
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
const targets = /* @__PURE__ */ new Map();
|
|
160
|
+
this._targetApplogs = rollingFilter(currentThread, { vl: this.en });
|
|
161
|
+
this._targetMap = new SubscribableImpl(
|
|
162
|
+
targets,
|
|
163
|
+
() => this._targetApplogs.subscribe((event) => {
|
|
164
|
+
if (isInitEvent(event)) {
|
|
165
|
+
targets.clear();
|
|
166
|
+
for (const log of event.init) {
|
|
167
|
+
let ats = targets.get(log.en);
|
|
168
|
+
if (!ats) {
|
|
169
|
+
ats = /* @__PURE__ */ new Set();
|
|
170
|
+
targets.set(log.en, ats);
|
|
171
|
+
}
|
|
172
|
+
ats.add(log.at);
|
|
173
|
+
}
|
|
174
|
+
} else {
|
|
175
|
+
for (const log of event.added) {
|
|
176
|
+
let ats = targets.get(log.en);
|
|
177
|
+
if (!ats) {
|
|
178
|
+
ats = /* @__PURE__ */ new Set();
|
|
179
|
+
targets.set(log.en, ats);
|
|
180
|
+
}
|
|
181
|
+
ats.add(log.at);
|
|
182
|
+
}
|
|
183
|
+
if (event.removed) {
|
|
184
|
+
for (const log of event.removed) {
|
|
185
|
+
const ats = targets.get(log.en);
|
|
186
|
+
if (ats) {
|
|
187
|
+
ats.delete(log.at);
|
|
188
|
+
if (ats.size === 0) targets.delete(log.en);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
this._targetMap._set(targets);
|
|
194
|
+
}, "derived")
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
en;
|
|
198
|
+
thread;
|
|
199
|
+
/** Per-instance signal storage */
|
|
200
|
+
_signals = /* @__PURE__ */ new Map();
|
|
201
|
+
_signalAdapter = signalAdapter;
|
|
202
|
+
/** Reactive map for applog tracking: vl === this.en */
|
|
203
|
+
_targetMap;
|
|
204
|
+
/** Applogs in the current thread where vl === this.en */
|
|
205
|
+
_targetApplogs;
|
|
206
|
+
/** Thread scoped to this entity */
|
|
207
|
+
get entityThread() {
|
|
208
|
+
return rollingFilter(this.thread, { en: this.en });
|
|
209
|
+
}
|
|
210
|
+
/** Applogs in the current thread where vl === this.en */
|
|
211
|
+
get targetApplogs() {
|
|
212
|
+
return this._targetApplogs;
|
|
213
|
+
}
|
|
214
|
+
/** Set of entities that have an applog with this.en as the vl */
|
|
215
|
+
get targettedBy() {
|
|
216
|
+
return new Set(this._targetMap.value.keys());
|
|
217
|
+
}
|
|
218
|
+
/** Set of at strings from applogs where vl === this.en */
|
|
219
|
+
get targettedVia() {
|
|
220
|
+
const via = /* @__PURE__ */ new Set();
|
|
221
|
+
for (const ats of this._targetMap.value.values()) {
|
|
222
|
+
for (const at of ats) via.add(at);
|
|
223
|
+
}
|
|
224
|
+
return via;
|
|
225
|
+
}
|
|
226
|
+
/** Whether this entity is soft-deleted */
|
|
227
|
+
get isDeleted() {
|
|
228
|
+
let signal = this._signals.get("__isDeleted");
|
|
229
|
+
if (!signal) {
|
|
230
|
+
const currentThread = makeCurrent(this.thread, options.makeCurrentThread);
|
|
231
|
+
const subscribable = liveEntityAt(currentThread, this.en, "isDeleted");
|
|
232
|
+
signal = this._signalAdapter.createGetter(subscribable);
|
|
233
|
+
this._signals.set("__isDeleted", signal);
|
|
234
|
+
}
|
|
235
|
+
return !!signal();
|
|
236
|
+
}
|
|
237
|
+
/** Soft-delete this entity */
|
|
238
|
+
setDeleted(thread = this.thread) {
|
|
239
|
+
const applog = { en: this.en, at: "isDeleted", vl: true };
|
|
240
|
+
persistFn(thread, [applog]);
|
|
241
|
+
}
|
|
242
|
+
/** Get a builder for updating this entity */
|
|
243
|
+
buildUpdate(init = {}) {
|
|
244
|
+
return new ObjectBuilder(init, this.en, entityPrefix);
|
|
245
|
+
}
|
|
246
|
+
/** Description for debugging */
|
|
247
|
+
get description() {
|
|
248
|
+
return `${vmName}VM(en=${this.en})`;
|
|
249
|
+
}
|
|
250
|
+
/** Get the full entity state as a plain object */
|
|
251
|
+
toJSON() {
|
|
252
|
+
const result = {};
|
|
253
|
+
for (const attr of attrDefs) {
|
|
254
|
+
const value = this[attr.name];
|
|
255
|
+
if (value !== void 0 && value !== null) {
|
|
256
|
+
result[attr.name] = value;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
return result;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
function getVMClass(en, thread) {
|
|
263
|
+
if (!en || typeof en !== "string") throw new Error(`[${vmName}VM.get] invalid en: ${en}`);
|
|
264
|
+
if (!thread) throw new Error(`[${vmName}VM.get] no thread provided`);
|
|
265
|
+
const threadMap = getInstancesForThread(thread);
|
|
266
|
+
let entityMap = threadMap.get(vmName);
|
|
267
|
+
if (!entityMap) {
|
|
268
|
+
entityMap = /* @__PURE__ */ new Map();
|
|
269
|
+
threadMap.set(vmName, entityMap);
|
|
270
|
+
}
|
|
271
|
+
const existing = entityMap.get(en);
|
|
272
|
+
if (existing) return existing;
|
|
273
|
+
const vm = new ViewModel(en, thread, false);
|
|
274
|
+
entityMap.set(en, vm);
|
|
275
|
+
return vm;
|
|
276
|
+
}
|
|
277
|
+
function buildNewEntity(init = {}, en) {
|
|
278
|
+
const entityId = en ?? genId(init);
|
|
279
|
+
return ObjectBuilder.create(init, entityId, entityPrefix);
|
|
280
|
+
}
|
|
281
|
+
const VMClass = ViewModel;
|
|
282
|
+
VMClass.get = getVMClass;
|
|
283
|
+
VMClass.buildNew = buildNewEntity;
|
|
284
|
+
VMClass.vmName = vmName;
|
|
285
|
+
VMClass.entityPrefix = entityPrefix;
|
|
286
|
+
return VMClass;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// src/viewmodel/schema-adapter.ts
|
|
290
|
+
function createAdapterFromAttributes(config) {
|
|
291
|
+
return {
|
|
292
|
+
getAttributeDefs: () => config.attributes,
|
|
293
|
+
getDefaults: () => config.defaults ?? {},
|
|
294
|
+
getEntityPrefix: () => config.entityPrefix
|
|
295
|
+
};
|
|
296
|
+
}
|
|
297
|
+
function buildAtPath(entityPrefix, attrName) {
|
|
298
|
+
return `${entityPrefix}/${attrName}`;
|
|
299
|
+
}
|
|
300
|
+
export {
|
|
301
|
+
DefaultSignalAdapter,
|
|
302
|
+
GlobalVMInstances,
|
|
303
|
+
ObjectBuilder,
|
|
304
|
+
buildAtPath,
|
|
305
|
+
createAdapterFromAttributes,
|
|
306
|
+
createArkTypeAdapter,
|
|
307
|
+
createTypeBoxAdapter,
|
|
308
|
+
createTypiaAdapter,
|
|
309
|
+
createViewModelFactory,
|
|
310
|
+
createZodAdapter,
|
|
311
|
+
getInstancesForThread
|
|
312
|
+
};
|
|
313
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/viewmodel/builder.ts","../../src/viewmodel/types.ts","../../src/viewmodel/factory.ts","../../src/viewmodel/schema-adapter.ts"],"sourcesContent":["import type { ApplogForInsertOptionalAgent, ApplogValue, EntityID } from '../applog/datom-types.ts'\n\n/**\n * Generic ObjectBuilder for type-safe entity creation and updates.\n *\n * Provides a fluent API to build a set of applogs for an entity.\n * Modeled after the note3 builder pattern.\n *\n * Usage:\n * ```ts\n * ObjectBuilder.create({ name: 'foo' })\n * .update({ type: 'bar' })\n * .build(thread)\n * ```\n */\nexport class ObjectBuilder<\n\tTARGET extends Record<string, ApplogValue> = Record<string, ApplogValue>,\n> {\n\tconstructor(\n\t\tprivate _data: Partial<TARGET>,\n\t\tpublic en: EntityID | null = null,\n\t\tprivate _atPrefix: string | null = null,\n\t\tprivate _atOverrides: Partial<Record<keyof TARGET, string>> = {},\n\t) {}\n\n\t/**\n\t * Create a new builder with initial data.\n\t */\n\tstatic create<TARGET extends Record<string, ApplogValue>>(\n\t\tinit: Partial<TARGET> = {},\n\t\ten?: EntityID,\n\t\tatPrefix?: string,\n\t\tatOverrides?: Partial<Record<keyof TARGET, string>>,\n\t): ObjectBuilder<TARGET> {\n\t\treturn new ObjectBuilder<TARGET>(init, en, atPrefix, atOverrides)\n\t}\n\n\t/**\n\t * Update the data being built.\n\t */\n\tupdate(updateObj: Partial<TARGET>): this {\n\t\tObject.assign(this._data, updateObj)\n\t\treturn this\n\t}\n\n\t/**\n\t * Build the applogs for this entity, optionally resolving against a thread.\n\t */\n\tbuild(thread?: {\n\t\tinsert(applogs: ApplogForInsertOptionalAgent[]): ApplogForInsertOptionalAgent[]\n\t}): ApplogForInsertOptionalAgent[] {\n\t\tconst atPrefix = this._atPrefix\n\t\tconst applogs: ApplogForInsertOptionalAgent[] = []\n\n\t\tfor (const [key, value] of Object.entries(this._data) as [keyof TARGET, ApplogValue][]) {\n\t\t\tif (value === undefined) continue\n\t\t\tconst atOverride = this._atOverrides[key]\n\t\t\tconst at = atOverride ?? (atPrefix ? `${atPrefix}/${String(key)}` : String(key))\n\t\t\tapplogs.push({ en: this.en!, at, vl: value })\n\t\t}\n\n\t\treturn applogs\n\t}\n\n\t/**\n\t * Get the raw data being built.\n\t */\n\tget data(): Partial<TARGET> {\n\t\treturn { ...this._data }\n\t}\n}\n","import type { ApplogValue, EntityID } from '../applog/datom-types.ts'\nimport type { Thread } from '../thread/basic.ts'\nimport type { ObjectBuilder } from './builder.ts'\n\n// ═══════════════════════════════════════════════════════════════\n// Attribute Definition\n// ═══════════════════════════════════════════════════════════════\n\nexport interface VMAttributeDef {\n\t/** Attribute name (short: e.g. 'name', 'content') */\n\tname: string\n\t/** The wovin at-path used in applogs (e.g. 'block/content', 'speaker/name') */\n\tatPath: string\n\t/** Whether the attribute is optional */\n\toptional: boolean\n\t/** Default value if not present in applogs */\n\tdefaultValue?: ApplogValue\n}\n\n// ═══════════════════════════════════════════════════════════════\n// Schema Adapter Interface\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * A schema adapter converts a type-system schema (TypeBox, Zod, ArkType, Typia)\n * into a uniform representation that the VM factory can consume.\n */\nexport interface ISchemaAdapter<T extends Record<string, ApplogValue> = Record<string, ApplogValue>> {\n\t/** Get the attribute definitions for this schema */\n\tgetAttributeDefs(): VMAttributeDef[]\n\n\t/** Get default values for all attributes */\n\tgetDefaults(): Partial<T>\n\n\t/** Optional: runtime validator */\n\tcreateValidator?(): (value: unknown) => value is T\n\n\t/** The entity prefix used for wovin at-paths (e.g. 'block', 'speaker') */\n\tgetEntityPrefix(): string\n}\n\n// ═══════════════════════════════════════════════════════════════\n// ViewModel Factory Options\n// ═══════════════════════════════════════════════════════════════\n\nexport interface ViewModelFactoryOptions<T extends Record<string, ApplogValue>> {\n\t/** The schema adapter for this entity type */\n\tadapter: ISchemaAdapter<T>\n\n\t/** How to get the default thread context */\n\tgetDefaultThread?: () => Thread\n\n\t/** EntityID generation length (default: 7) */\n\tentityIdLength?: number\n\n\t/** Function to generate entity IDs */\n\tgenerateEntityId?: (data: Partial<T> & { ts?: string }) => EntityID\n\n\t/** Override the entity prefix for at-paths */\n\tentityPrefix?: string\n\n\t/** Optional function to make thread current-state (lastWriteWins) */\n\tmakeCurrentThread?: (thread: Thread) => Thread\n\n\t/** Name for the VM type (used in logging and debugging) */\n\tvmName?: string\n\n\t/**\n\t * Signal adapter for framework-specific reactivity.\n\t * Default: non-reactive (direct Subscribable reads).\n\t * For SolidJS, pass createSolidSignalAdapter() from @wovin/ui-solid/viewmodel.\n\t */\n\tsignalAdapter?: ISignalAdapter\n}\n\n// ═══════════════════════════════════════════════════════════════\n// ViewModel Instance Interface\n// ═══════════════════════════════════════════════════════════════\n\nexport interface IVMInstance<T extends Record<string, ApplogValue> = Record<string, ApplogValue>> {\n\treadonly en: EntityID\n\treadonly thread: Thread\n\n\t/** Get a builder for updating this entity */\n\tbuildUpdate(init?: Partial<T>): ObjectBuilder<T>\n\n\t/** Soft-delete this entity */\n\tsetDeleted(thread?: Thread): void\n\n\t/** Whether this entity is deleted */\n\treadonly isDeleted: boolean\n\n\t/** The entity thread filtered to this entity */\n\treadonly entityThread: Thread\n}\n\n// ═══════════════════════════════════════════════════════════════\n// ViewModel Class Interface (what the factory returns)\n// ═══════════════════════════════════════════════════════════════\n\nexport interface IVMClass<T extends Record<string, ApplogValue> = Record<string, ApplogValue>> {\n\tnew(en: EntityID, thread: Thread, ...args: any[]): IVMInstance<T>\n\n\t/** Get or create a VM instance for the given entity ID and thread */\n\tget(en: EntityID, thread: Thread): IVMInstance<T>\n\n\t/** Create a new entity builder */\n\tbuildNew(init?: Partial<T>, en?: EntityID): ObjectBuilder<T>\n}\n\n// ═══════════════════════════════════════════════════════════════\n// Signal Adapter Interface (framework-specific reactivity)\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * A signal adapter abstracts how the VM creates reactive signals for each attribute.\n * The core factory uses a default (non-reactive) implementation.\n * Framework-specific extensions (Solid, Vue, etc.) override this.\n */\nexport interface ISignalAdapter {\n\t/**\n\t * Create a reactive getter for a subscribable value.\n\t * Returns a function that returns the current value.\n\t * In the default implementation, this is just () => subscribable.value.\n\t */\n\tcreateGetter<T>(subscribable: { value: T; subscribe(cb: () => void): () => void }): () => T\n\n\t/**\n\t * Create a writable signal for an attribute.\n\t * Returns [get, set] pair.\n\t * Default implementation stores and returns value directly.\n\t */\n\tcreateWritable<T>(initial: T): [() => T, (v: T) => void]\n}\n\n// ═══════════════════════════════════════════════════════════════\n// Shared state maps\n// ═══════════════════════════════════════════════════════════════\n\nexport type VMInstanceMap<T> = Map<EntityID, T>\n\n/** WeakMap from Thread → Map<VMName, Map<EntityID, VMInstance>> */\nexport const GlobalVMInstances = new WeakMap<Thread, Map<string, Map<EntityID, unknown>>>()\n\nexport function getInstancesForThread(thread: Thread): Map<string, Map<EntityID, unknown>> {\n\tlet threadMap = GlobalVMInstances.get(thread)\n\tif (!threadMap) {\n\t\tthreadMap = new Map()\n\t\tGlobalVMInstances.set(thread, threadMap)\n\t}\n\treturn threadMap\n}\n","import type { ApplogForInsertOptionalAgent, ApplogValue, EntityID } from '../applog/datom-types.ts'\nimport { EntityID_LENGTH } from '../applog/datom-types.ts'\nimport { dateNowIso, getHashID } from '../applog/applog-utils.ts'\nimport { ensureTsPvAndFinalizeApplog } from '../applog/applog-helpers.ts'\nimport { lastWriteWins, liveEntityAt } from '../query/basic.ts'\nimport { SubscribableImpl } from '../query/subscribable.ts'\nimport type { Thread } from '../thread/basic.ts'\nimport { isInitEvent } from '../thread/basic.ts'\nimport { rollingFilter } from '../thread/filters.ts'\nimport { ObjectBuilder } from './builder.ts'\nimport {\n\ttype ISignalAdapter,\n\ttype IVMInstance,\n\ttype ViewModelFactoryOptions,\n\ttype VMInstanceMap,\n\tgetInstancesForThread,\n} from './types.ts'\n\n// ═══════════════════════════════════════════════════════════════\n// Default (non-reactive) Signal Adapter\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * Default signal adapter — reads Subscribable values directly (snapshot).\n * Subscribe-once to keep .value current. No framework reactivity.\n */\nexport const DefaultSignalAdapter: ISignalAdapter = {\n\tcreateGetter<T>(subscribable: { value: T; subscribe(cb: () => void): () => void }): () => T {\n\t\t// Subscribe once to activate upstream (lazy activation)\n\t\tsubscribable.subscribe(() => {})\n\t\treturn () => subscribable.value\n\t},\n\tcreateWritable<T>(initial: T): [() => T, (v: T) => void] {\n\t\tlet current = initial\n\t\treturn [() => current, (v: T) => { current = v }]\n\t},\n}\n\n// ═══════════════════════════════════════════════════════════════\n// makeCurrentThread - convenience wrapper\n// ═══════════════════════════════════════════════════════════════\n\nfunction makeCurrent(thread: Thread, fn?: ViewModelFactoryOptions<any>['makeCurrentThread']): Thread {\n\tif (fn) return fn(thread)\n\treturn lastWriteWins(thread, { tolerateAlreadyFiltered: true })\n}\n\n// ═══════════════════════════════════════════════════════════════\n// Default persist function\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * Default applog persistence: finalize applogs and push into thread.\n * Works with WriteableThread (which has insertRaw).\n */\nfunction defaultPersistApplogs(thread: Thread, applogs: ApplogForInsertOptionalAgent[]): void {\n\tconst finalized = applogs.map(log => ensureTsPvAndFinalizeApplog(log as any, thread))\n\tconst writable = thread as any\n\tif (typeof writable.insertRaw === 'function') {\n\t\twritable.insertRaw(finalized)\n\t} else if (typeof writable.insert === 'function') {\n\t\twritable.insert(applogs)\n\t} else {\n\t\t// Read-only thread — just finalize but don't persist\n\t\t// This is fine for optimistic UI or read-only scenarios\n\t}\n}\n\n// ═══════════════════════════════════════════════════════════════\n// createViewModelFactory - THE MAIN FACTORY\n// ═══════════════════════════════════════════════════════════════\n\n/**\n * Create a ViewModel factory for a given schema.\n *\n * Returns a base class that can be extended or used directly.\n * The class provides:\n * - Static `get(en, thread)` for singleton access\n * - Static `buildNew(init, en)` for creating entities\n * - Instance `buildUpdate(init)` for updating entities\n * - Lazy reactive property accessors per attribute\n * - `setDeleted()` for soft deletion\n *\n * Framework-specific reactivity (Solid, Vue, etc.) can be added by\n * providing a custom `ISignalAdapter` in the options.\n *\n * @param options - Factory configuration\n * @returns A VM class constructor with static methods\n */\nexport function createViewModelFactory<T extends Record<string, ApplogValue>>(\n\toptions: ViewModelFactoryOptions<T>,\n) {\n\tconst {\n\t\tadapter,\n\t\tentityPrefix = adapter.getEntityPrefix(),\n\t\tentityIdLength = EntityID_LENGTH,\n\t\tgenerateEntityId,\n\t\tvmName = entityPrefix,\n\t\tsignalAdapter = DefaultSignalAdapter,\n\t} = options\n\n\tconst attrDefs = adapter.getAttributeDefs()\n\tconst defaults = adapter.getDefaults()\n\n\t// Entity ID generation\n\tconst genId = generateEntityId ?? ((data: Partial<T> & { ts?: string }) =>\n\t\tgetHashID({ ...data, ts: data.ts ?? dateNowIso() }, entityIdLength) as EntityID)\n\n\t// Persist function (can be overridden by extending class)\n\tconst persistFn = (thread: Thread, applogs: ApplogForInsertOptionalAgent[]) => {\n\t\tdefaultPersistApplogs(thread, applogs)\n\t}\n\n\t// ═══════════════════════════════════════════════════════════════\n\t// The ViewModel Class\n\t// ═══════════════════════════════════════════════════════════════\n\tclass ViewModel {\n\t\t/** Per-instance signal storage */\n\t\t_signals = new Map<string, () => ApplogValue>()\n\t\t_signalAdapter: ISignalAdapter = signalAdapter\n\n\t\t/** Reactive map for applog tracking: vl === this.en */\n\t\t_targetMap: SubscribableImpl<Map<EntityID, Set<string>>>\n\n\t\t/** Applogs in the current thread where vl === this.en */\n\t\t_targetApplogs: Thread\n\n\t\tconstructor(\n\t\t\tpublic en: EntityID,\n\t\t\tpublic thread: Thread,\n\t\t\tskipInit?: boolean,\n\t\t) {\n\t\t\tif (skipInit) return\n\n\t\t\tconst currentThread = makeCurrent(thread, options.makeCurrentThread)\n\n\t\t\t// Define lazy getters/setters for each attribute\n\t\t\tfor (const attr of attrDefs) {\n\t\t\t\tif (attr.name === 'en') continue\n\n\t\t\t\tconst attrName = attr.name\n\t\t\t\tconst atPath = attr.atPath\n\t\t\t\tconst defaultValue = attr.defaultValue ?? defaults?.[attrName as keyof T]\n\n\t\t\t\tObject.defineProperty(this, attrName, {\n\t\t\t\t\tget(this: ViewModel) {\n\t\t\t\t\t\tlet signal = this._signals.get(attrName)\n\t\t\t\t\t\tif (!signal) {\n\t\t\t\t\t\t\tconst subscribable = liveEntityAt(currentThread, this.en, atPath)\n\t\t\t\t\t\t\tsignal = this._signalAdapter.createGetter(subscribable)\n\t\t\t\t\t\t\tthis._signals.set(attrName, signal)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst value = signal()\n\t\t\t\t\t\treturn value ?? (defaultValue as ApplogValue | undefined) ?? value\n\t\t\t\t\t},\n\t\t\t\t\tset(this: ViewModel, v: ApplogValue) {\n\t\t\t\t\t\tconst applog: ApplogForInsertOptionalAgent = { en: this.en, at: atPath, vl: v }\n\t\t\t\t\t\tpersistFn(this.thread, [applog])\n\t\t\t\t\t},\n\t\t\t\t\tenumerable: true,\n\t\t\t\t\tconfigurable: true,\n\t\t\t\t})\n\t\t\t}\n\n\t\t\t// ── Applog tracking: applogs where vl === this.en ────────────\n\t\t\tconst targets = new Map<EntityID, Set<string>>()\n\n\t\t\tthis._targetApplogs = rollingFilter(currentThread, { vl: this.en })\n\t\t\tthis._targetMap = new SubscribableImpl(\n\t\t\t\ttargets,\n\t\t\t\t() => this._targetApplogs.subscribe((event) => {\n\t\t\t\t\tif (isInitEvent(event)) {\n\t\t\t\t\t\ttargets.clear()\n\t\t\t\t\t\tfor (const log of event.init) {\n\t\t\t\t\t\t\tlet ats = targets.get(log.en)\n\t\t\t\t\t\t\tif (!ats) {\n\t\t\t\t\t\t\t\tats = new Set()\n\t\t\t\t\t\t\t\ttargets.set(log.en, ats)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tats.add(log.at)\n\t\t\t\t\t\t}\n\t\t\t\t\t} else {\n\t\t\t\t\t\tfor (const log of event.added) {\n\t\t\t\t\t\t\tlet ats = targets.get(log.en)\n\t\t\t\t\t\t\tif (!ats) {\n\t\t\t\t\t\t\t\tats = new Set()\n\t\t\t\t\t\t\t\ttargets.set(log.en, ats)\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tats.add(log.at)\n\t\t\t\t\t\t}\n\t\t\t\t\t\tif (event.removed) {\n\t\t\t\t\t\t\tfor (const log of event.removed) {\n\t\t\t\t\t\t\t\tconst ats = targets.get(log.en)\n\t\t\t\t\t\t\t\tif (ats) {\n\t\t\t\t\t\t\t\t\tats.delete(log.at)\n\t\t\t\t\t\t\t\t\tif (ats.size === 0) targets.delete(log.en)\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tthis._targetMap._set(targets)\n\t\t\t\t}, 'derived'),\n\t\t\t)\n\t\t}\n\n\t\t/** Thread scoped to this entity */\n\t\tget entityThread(): Thread {\n\t\t\treturn rollingFilter(this.thread, { en: this.en })\n\t\t}\n\n\t\t/** Applogs in the current thread where vl === this.en */\n\t\tget targetApplogs(): Thread {\n\t\t\treturn this._targetApplogs\n\t\t}\n\n\t\t/** Set of entities that have an applog with this.en as the vl */\n\t\tget targettedBy(): Set<EntityID> {\n\t\t\treturn new Set(this._targetMap.value.keys())\n\t\t}\n\n\t\t/** Set of at strings from applogs where vl === this.en */\n\t\tget targettedVia(): Set<string> {\n\t\t\tconst via = new Set<string>()\n\t\t\tfor (const ats of this._targetMap.value.values()) {\n\t\t\t\tfor (const at of ats) via.add(at)\n\t\t\t}\n\t\t\treturn via\n\t\t}\n\n\t\t/** Whether this entity is soft-deleted */\n\t\tget isDeleted(): boolean {\n\t\t\tlet signal = this._signals.get('__isDeleted')\n\t\t\tif (!signal) {\n\t\t\t\tconst currentThread = makeCurrent(this.thread, options.makeCurrentThread)\n\t\t\t\tconst subscribable = liveEntityAt(currentThread, this.en, 'isDeleted')\n\t\t\t\tsignal = this._signalAdapter.createGetter(subscribable)\n\t\t\t\tthis._signals.set('__isDeleted', signal)\n\t\t\t}\n\t\t\treturn !!signal()\n\t\t}\n\n\t\t/** Soft-delete this entity */\n\t\tsetDeleted(thread = this.thread): void {\n\t\t\tconst applog: ApplogForInsertOptionalAgent = { en: this.en, at: 'isDeleted', vl: true }\n\t\t\tpersistFn(thread, [applog])\n\t\t}\n\n\t\t/** Get a builder for updating this entity */\n\t\tbuildUpdate(init: Partial<T> = {}): ObjectBuilder<T> {\n\t\t\treturn new ObjectBuilder<T>(init, this.en, entityPrefix)\n\t\t}\n\n\t\t/** Description for debugging */\n\t\tget description(): string {\n\t\t\treturn `${vmName}VM(en=${this.en})`\n\t\t}\n\n\t\t/** Get the full entity state as a plain object */\n\t\ttoJSON(): Partial<T> {\n\t\t\tconst result: Record<string, ApplogValue> = {}\n\t\t\tfor (const attr of attrDefs) {\n\t\t\t\tconst value = (this as any)[attr.name]\n\t\t\t\tif (value !== undefined && value !== null) {\n\t\t\t\t\tresult[attr.name] = value\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn result as Partial<T>\n\t\t}\n\t}\n\n\t// ── Static Methods ────────────────────────────────────────────\n\n\t/**\n\t * Get or create a VM instance for the given entity and thread.\n\t * Implements the singleton pattern — returns cached instance if one exists.\n\t */\n\tfunction getVMClass(en: EntityID, thread: Thread): InstanceType<typeof ViewModel> {\n\t\tif (!en || typeof en !== 'string') throw new Error(`[${vmName}VM.get] invalid en: ${en}`)\n\t\tif (!thread) throw new Error(`[${vmName}VM.get] no thread provided`)\n\n\t\tconst threadMap = getInstancesForThread(thread)\n\t\tlet entityMap = threadMap.get(vmName) as VMInstanceMap<InstanceType<typeof ViewModel>> | undefined\n\t\tif (!entityMap) {\n\t\t\tentityMap = new Map()\n\t\t\tthreadMap.set(vmName, entityMap)\n\t\t}\n\n\t\tconst existing = entityMap.get(en)\n\t\tif (existing) return existing\n\n\t\tconst vm = new ViewModel(en, thread, false) as InstanceType<typeof ViewModel>\n\t\tentityMap.set(en, vm)\n\t\treturn vm\n\t}\n\n\t/**\n\t * Create a builder for constructing a new entity.\n\t */\n\tfunction buildNewEntity(init: Partial<T> = {}, en?: EntityID): ObjectBuilder<T> {\n\t\tconst entityId = en ?? genId(init as any)\n\t\treturn ObjectBuilder.create<T>(init, entityId, entityPrefix)\n\t}\n\n\t// Attach static methods to the class\n\tconst VMClass: any = ViewModel\n\tVMClass.get = getVMClass\n\tVMClass.buildNew = buildNewEntity\n\tVMClass.vmName = vmName\n\tVMClass.entityPrefix = entityPrefix\n\n\treturn VMClass as unknown as {\n\t\tnew(\n\t\t\ten: EntityID,\n\t\t\tthread: Thread,\n\t\t\tskipInit?: boolean,\n\t\t): IVMInstance<T> & T\n\n\t\t/** Get or create a VM instance for the given entity ID */\n\t\tget(en: EntityID, thread: Thread): IVMInstance<T> & T\n\n\t\t/** Create a builder for a new entity */\n\t\tbuildNew(init?: Partial<T>, en?: EntityID): ObjectBuilder<T>\n\n\t\t/** The VM name (for debugging) */\n\t\treadonly vmName: string\n\n\t\t/** The entity prefix used for at-paths */\n\t\treadonly entityPrefix: string\n\t}\n}\n","import type { ApplogValue } from '../applog/datom-types.ts'\nimport type { ISchemaAdapter, VMAttributeDef } from './types.ts'\n\n/**\n * Helper to create a simple schema adapter from a plain attribute list.\n * Useful for quick VM definitions or when you don't have a formal schema library.\n */\nexport function createAdapterFromAttributes<T extends Record<string, ApplogValue>>(\n\tconfig: {\n\t\tattributes: VMAttributeDef[]\n\t\tentityPrefix: string\n\t\tdefaults?: Partial<T>\n\t},\n): ISchemaAdapter<T> {\n\treturn {\n\t\tgetAttributeDefs: () => config.attributes,\n\t\tgetDefaults: () => (config.defaults ?? {}) as Partial<T>,\n\t\tgetEntityPrefix: () => config.entityPrefix,\n\t}\n}\n\n/**\n * Helper to build at-paths from attribute names and an entity prefix.\n */\nexport function buildAtPath(entityPrefix: string, attrName: string): string {\n\treturn `${entityPrefix}/${attrName}`\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { ApplogValue } from '../applog/datom-types.ts';
|
|
2
|
+
import type { ISchemaAdapter, VMAttributeDef } from './types.ts';
|
|
3
|
+
/**
|
|
4
|
+
* Helper to create a simple schema adapter from a plain attribute list.
|
|
5
|
+
* Useful for quick VM definitions or when you don't have a formal schema library.
|
|
6
|
+
*/
|
|
7
|
+
export declare function createAdapterFromAttributes<T extends Record<string, ApplogValue>>(config: {
|
|
8
|
+
attributes: VMAttributeDef[];
|
|
9
|
+
entityPrefix: string;
|
|
10
|
+
defaults?: Partial<T>;
|
|
11
|
+
}): ISchemaAdapter<T>;
|
|
12
|
+
/**
|
|
13
|
+
* Helper to build at-paths from attribute names and an entity prefix.
|
|
14
|
+
*/
|
|
15
|
+
export declare function buildAtPath(entityPrefix: string, attrName: string): string;
|
|
16
|
+
//# sourceMappingURL=schema-adapter.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"schema-adapter.d.ts","sourceRoot":"","sources":["../../src/viewmodel/schema-adapter.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAA;AAC3D,OAAO,KAAK,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAEhE;;;GAGG;AACH,wBAAgB,2BAA2B,CAAC,CAAC,SAAS,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,EAChF,MAAM,EAAE;IACP,UAAU,EAAE,cAAc,EAAE,CAAA;IAC5B,YAAY,EAAE,MAAM,CAAA;IACpB,QAAQ,CAAC,EAAE,OAAO,CAAC,CAAC,CAAC,CAAA;CACrB,GACC,cAAc,CAAC,CAAC,CAAC,CAMnB;AAED;;GAEG;AACH,wBAAgB,WAAW,CAAC,YAAY,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAE1E"}
|
|
@@ -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.0",
|
|
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": {
|
|
@@ -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).
|
package/src/ipfs/car.ts
CHANGED
|
@@ -72,8 +72,14 @@ export async function decodePubFromBlocks(
|
|
|
72
72
|
pubLogsArray = await unchunkApplogsBlock(applogsBlock, blockStore)
|
|
73
73
|
// Info only from first (most recent) snapshot
|
|
74
74
|
if (!firstInfo) {
|
|
75
|
-
|
|
76
|
-
|
|
75
|
+
const decoded = (await getDecodedBlock(blockStore, root.info)) as SnapBlockLogs
|
|
76
|
+
if (decoded) {
|
|
77
|
+
firstInfo = decoded
|
|
78
|
+
DEBUG(`new format - infoLogs`, firstInfo.logs.map(l => ({ [l.toString()]: l })))
|
|
79
|
+
} else {
|
|
80
|
+
WARN(`[decodePubFromBlocks] info block not found for ${root.info}, using empty info`)
|
|
81
|
+
firstInfo = { logs: [] }
|
|
82
|
+
}
|
|
77
83
|
}
|
|
78
84
|
// TODO: verify signatures
|
|
79
85
|
} 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
|
+
}
|