@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,206 @@
1
+ /**
2
+ * ═══════════════════════════════════════════════════════════════
3
+ * Schema Adapter Examples
4
+ * ═══════════════════════════════════════════════════════════════
5
+ *
6
+ * This file demonstrates how to use each schema adapter with the
7
+ * generic ViewModel factory. Uncomment the adapter you want to use.
8
+ *
9
+ * Each section is self-contained and shows:
10
+ * 1. Schema definition
11
+ * 2. Adapter creation
12
+ * 3. ViewModel factory usage
13
+ * 4. Basic VM usage
14
+ *
15
+ * Note: Not all schema libraries are installed as dependencies.
16
+ * ✓ TypeBox — peer dependency of @wovin/core (available)
17
+ * ✓ Zod — dependency of @wovin/core (available)
18
+ * ✗ ArkType — NOT installed (example shown for pattern reference)
19
+ * ✗ Typia — NOT installed (example shown for pattern reference)
20
+ */
21
+
22
+ import { createViewModelFactory, ObjectBuilder, type ISchemaAdapter, type VMAttributeDef } from '../index.ts'
23
+ import type { Thread } from '../../thread/basic.ts'
24
+ import type { ApplogValue, EntityID } from '../../applog/datom-types.ts'
25
+
26
+ // ═══════════════════════════════════════════════════════════════
27
+ // Shared helpers for examples
28
+ // ═══════════════════════════════════════════════════════════════
29
+
30
+ /** Dummy thread for examples */
31
+ function createMockThread(): Thread {
32
+ return { name: 'mock', applogs: [], filters: [], subscribe: () => () => {} } as unknown as Thread
33
+ }
34
+
35
+ interface Note extends Record<string, ApplogValue> {
36
+ title: string
37
+ body: string
38
+ tags?: string
39
+ }
40
+
41
+ // ═══════════════════════════════════════════════════════════════
42
+ // 1. TypeBox Adapter Example
43
+ // ═══════════════════════════════════════════════════════════════
44
+
45
+ // import { Type } from '@sinclair/typebox'
46
+ // import { Static } from '@sinclair/typebox'
47
+ // import { createTypeBoxAdapter } from '../adapters/typebox.ts'
48
+
49
+ export function typeBoxExample() {
50
+ /*
51
+
52
+ const NoteSchema = Type.Object({
53
+ title: Type.String(),
54
+ body: Type.String(),
55
+ tags: Type.Optional(Type.String()),
56
+ })
57
+ type Note = Static<typeof NoteSchema>
58
+
59
+ const NoteVM = createViewModelFactory<Note>({
60
+ adapter: createTypeBoxAdapter(NoteSchema, 'note', {
61
+ atOverrides: {
62
+ title: 'note/title',
63
+ body: 'note/body',
64
+ tags: 'note/tags',
65
+ },
66
+ }),
67
+ vmName: 'Note',
68
+ })
69
+
70
+ const thread = createMockThread()
71
+ const note = NoteVM.get('abc123' as EntityID, thread)
72
+ console.log(note.title) // reactive read
73
+ note.title = 'Hello' // writes to thread via applog
74
+
75
+ */
76
+ }
77
+
78
+ // ═══════════════════════════════════════════════════════════════
79
+ // 2. Zod Adapter Example
80
+ // ═══════════════════════════════════════════════════════════════
81
+
82
+ // import { z } from 'zod'
83
+ // import { createZodAdapter } from '../adapters/zod.ts'
84
+
85
+ export function zodExample() {
86
+ /*
87
+
88
+ const NoteSchema = z.object({
89
+ title: z.string(),
90
+ body: z.string(),
91
+ tags: z.string().optional(),
92
+ })
93
+ type Note = z.infer<typeof NoteSchema>
94
+
95
+ const NoteVM = createViewModelFactory<Note>({
96
+ adapter: createZodAdapter(NoteSchema, 'note', {
97
+ atOverrides: {
98
+ title: 'note/title',
99
+ body: 'note/body',
100
+ tags: 'note/tags',
101
+ },
102
+ }),
103
+ vmName: 'Note',
104
+ })
105
+
106
+ const thread = createMockThread()
107
+ const note = NoteVM.get('abc123' as EntityID, thread)
108
+ console.log(note.title)
109
+ note.title = 'Hello'
110
+
111
+ */
112
+ }
113
+
114
+ // ═══════════════════════════════════════════════════════════════
115
+ // 3. ArkType Adapter Pattern
116
+ // ═══════════════════════════════════════════════════════════════
117
+ // ArkType is NOT a dependency. This shows the pattern if you install it.
118
+
119
+ // import { createArkTypeAdapter } from '../adapters/arktype.ts'
120
+
121
+ export function arkTypePattern() {
122
+ /*
123
+
124
+ // First, define attributes manually (ArkType doesn't expose runtime props)
125
+ const attrs: VMAttributeDef[] = [
126
+ { name: 'title', atPath: 'note/title', optional: false },
127
+ { name: 'body', atPath: 'note/body', optional: false },
128
+ { name: 'tags', atPath: 'note/tags', optional: true },
129
+ ]
130
+
131
+ // Then create the adapter with a validator
132
+ // import { type } from 'arktype'
133
+ // const NoteSchema = type({ title: 'string', body: 'string', 'tags?': 'string' })
134
+
135
+ const NoteVM = createViewModelFactory<Note>({
136
+ adapter: createArkTypeAdapter(
137
+ // NoteSchema, // your arktype validator
138
+ {} as any, // placeholder
139
+ 'note',
140
+ { attributes: attrs },
141
+ ),
142
+ vmName: 'Note',
143
+ })
144
+
145
+ */
146
+ }
147
+
148
+ // ═══════════════════════════════════════════════════════════════
149
+ // 4. Typia Adapter Pattern
150
+ // ═══════════════════════════════════════════════════════════════
151
+ // Typia is NOT a dependency. This shows the pattern if you install it.
152
+
153
+ // import { createTypiaAdapter } from '../adapters/typia.ts'
154
+
155
+ export function typiaPattern() {
156
+ /*
157
+
158
+ // Define attributes manually (Typia strips types at compile time)
159
+ const attrs: VMAttributeDef[] = [
160
+ { name: 'title', atPath: 'note/title', optional: false },
161
+ { name: 'body', atPath: 'note/body', optional: false },
162
+ { name: 'tags', atPath: 'note/tags', optional: true },
163
+ ]
164
+
165
+ // Then create the adapter with a typia validator
166
+ // import typia from 'typia'
167
+ // interface Note { title: string; body: string; tags?: string }
168
+ // const validate = typia.createValidate<Note>()
169
+
170
+ const NoteVM = createViewModelFactory<Note>({
171
+ adapter: createTypiaAdapter(
172
+ // validate, // your typia validator
173
+ {} as any, // placeholder
174
+ 'note',
175
+ { attributes: attrs },
176
+ ),
177
+ vmName: 'Note',
178
+ })
179
+
180
+ */
181
+ }
182
+
183
+ // ═══════════════════════════════════════════════════════════════
184
+ // 5. Minimal adapter (no schema library)
185
+ // ═══════════════════════════════════════════════════════════════
186
+
187
+ export function minimalNoSchemaExample() {
188
+ // You don't need a schema library at all — use createAdapterFromAttributes
189
+
190
+ const adapter: ISchemaAdapter<Note> = {
191
+ getAttributeDefs: (): VMAttributeDef[] => [
192
+ { name: 'title', atPath: 'note/title', optional: false },
193
+ { name: 'body', atPath: 'note/body', optional: false },
194
+ { name: 'tags', atPath: 'note/tags', optional: true },
195
+ ],
196
+ getDefaults: () => ({ title: '', body: '' }),
197
+ getEntityPrefix: () => 'note',
198
+ }
199
+
200
+ const NoteVM = createViewModelFactory<Note>({
201
+ adapter,
202
+ vmName: 'Note',
203
+ })
204
+
205
+ console.log('Minimal NoteVM created:', NoteVM.vmName)
206
+ }
@@ -0,0 +1,330 @@
1
+ import type { ApplogForInsertOptionalAgent, ApplogValue, EntityID } from '../applog/datom-types.ts'
2
+ import { EntityID_LENGTH } from '../applog/datom-types.ts'
3
+ import { dateNowIso, mintNewEntity } from '../applog/applog-utils.ts'
4
+ import { ensureTsPvAndFinalizeApplog } from '../applog/applog-helpers.ts'
5
+ import { lastWriteWins, liveEntityAt } from '../query/basic.ts'
6
+ import { SubscribableImpl } from '../query/subscribable.ts'
7
+ import type { Thread } from '../thread/basic.ts'
8
+ import { isInitEvent } from '../thread/basic.ts'
9
+ import { rollingFilter } from '../thread/filters.ts'
10
+ import { ObjectBuilder } from './builder.ts'
11
+ import {
12
+ type ISignalAdapter,
13
+ type IVMInstance,
14
+ type ViewModelFactoryOptions,
15
+ type VMInstanceMap,
16
+ getInstancesForThread,
17
+ } from './types.ts'
18
+
19
+ // ═══════════════════════════════════════════════════════════════
20
+ // Default (non-reactive) Signal Adapter
21
+ // ═══════════════════════════════════════════════════════════════
22
+
23
+ /**
24
+ * Default signal adapter — reads Subscribable values directly (snapshot).
25
+ * Subscribe-once to keep .value current. No framework reactivity.
26
+ */
27
+ export const DefaultSignalAdapter: ISignalAdapter = {
28
+ createGetter<T>(subscribable: { value: T; subscribe(cb: () => void): () => void }): () => T {
29
+ // Subscribe once to activate upstream (lazy activation)
30
+ subscribable.subscribe(() => {})
31
+ return () => subscribable.value
32
+ },
33
+ createWritable<T>(initial: T): [() => T, (v: T) => void] {
34
+ let current = initial
35
+ return [() => current, (v: T) => { current = v }]
36
+ },
37
+ }
38
+
39
+ // ═══════════════════════════════════════════════════════════════
40
+ // makeCurrentThread - convenience wrapper
41
+ // ═══════════════════════════════════════════════════════════════
42
+
43
+ function makeCurrent(thread: Thread, fn?: ViewModelFactoryOptions<any>['makeCurrentThread']): Thread {
44
+ if (fn) return fn(thread)
45
+ return lastWriteWins(thread, { tolerateAlreadyFiltered: true })
46
+ }
47
+
48
+ // ═══════════════════════════════════════════════════════════════
49
+ // Default persist function
50
+ // ═══════════════════════════════════════════════════════════════
51
+
52
+ /**
53
+ * Default applog persistence: finalize applogs and push into thread.
54
+ * Works with WriteableThread (which has insertRaw).
55
+ */
56
+ function defaultPersistApplogs(thread: Thread, applogs: ApplogForInsertOptionalAgent[]): void {
57
+ const finalized = applogs.map(log => ensureTsPvAndFinalizeApplog(log as any, thread))
58
+ const writable = thread as any
59
+ if (typeof writable.insertRaw === 'function') {
60
+ writable.insertRaw(finalized)
61
+ } else if (typeof writable.insert === 'function') {
62
+ writable.insert(applogs)
63
+ } else {
64
+ // Read-only thread — just finalize but don't persist
65
+ // This is fine for optimistic UI or read-only scenarios
66
+ }
67
+ }
68
+
69
+ // ═══════════════════════════════════════════════════════════════
70
+ // createViewModelFactory - THE MAIN FACTORY
71
+ // ═══════════════════════════════════════════════════════════════
72
+
73
+ /**
74
+ * Create a ViewModel factory for a given schema.
75
+ *
76
+ * Returns a base class that can be extended or used directly.
77
+ * The class provides:
78
+ * - Static `get(en, thread)` for singleton access
79
+ * - Static `buildNew(init, en)` for creating entities
80
+ * - Instance `buildUpdate(init)` for updating entities
81
+ * - Lazy reactive property accessors per attribute
82
+ * - `setDeleted()` for soft deletion
83
+ *
84
+ * Framework-specific reactivity (Solid, Vue, etc.) can be added by
85
+ * providing a custom `ISignalAdapter` in the options.
86
+ *
87
+ * @param options - Factory configuration
88
+ * @returns A VM class constructor with static methods
89
+ */
90
+ export function createViewModelFactory<T extends Record<string, ApplogValue>>(
91
+ options: ViewModelFactoryOptions<T>,
92
+ ) {
93
+ const {
94
+ adapter,
95
+ entityPrefix = adapter.getEntityPrefix(),
96
+ entityIdLength = EntityID_LENGTH,
97
+ generateEntityId,
98
+ vmName = entityPrefix,
99
+ signalAdapter = DefaultSignalAdapter,
100
+ } = options
101
+
102
+ const attrDefs = adapter.getAttributeDefs()
103
+ const defaults = adapter.getDefaults()
104
+
105
+ // Entity ID generation
106
+ const genId = generateEntityId ?? ((data: Partial<T> & { ts?: string }) =>
107
+ mintNewEntity({ ...data, ts: data.ts ?? dateNowIso() }, entityIdLength) as EntityID)
108
+
109
+ // Persist function (can be overridden by extending class)
110
+ const persistFn = (thread: Thread, applogs: ApplogForInsertOptionalAgent[]) => {
111
+ defaultPersistApplogs(thread, applogs)
112
+ }
113
+
114
+ // ═══════════════════════════════════════════════════════════════
115
+ // The ViewModel Class
116
+ // ═══════════════════════════════════════════════════════════════
117
+ class ViewModel {
118
+ /** Per-instance signal storage */
119
+ _signals = new Map<string, () => ApplogValue>()
120
+ _signalAdapter: ISignalAdapter = signalAdapter
121
+
122
+ /** Reactive map for applog tracking: vl === this.en */
123
+ _targetMap: SubscribableImpl<Map<EntityID, Set<string>>>
124
+
125
+ /** Applogs in the current thread where vl === this.en */
126
+ _targetApplogs: Thread
127
+
128
+ constructor(
129
+ public en: EntityID,
130
+ public thread: Thread,
131
+ skipInit?: boolean,
132
+ ) {
133
+ if (skipInit) return
134
+
135
+ const currentThread = makeCurrent(thread, options.makeCurrentThread)
136
+
137
+ // Define lazy getters/setters for each attribute
138
+ for (const attr of attrDefs) {
139
+ if (attr.name === 'en') continue
140
+
141
+ const attrName = attr.name
142
+ const atPath = attr.atPath
143
+ const defaultValue = attr.defaultValue ?? defaults?.[attrName as keyof T]
144
+
145
+ Object.defineProperty(this, attrName, {
146
+ get(this: ViewModel) {
147
+ let signal = this._signals.get(attrName)
148
+ if (!signal) {
149
+ const subscribable = liveEntityAt(currentThread, this.en, atPath)
150
+ signal = this._signalAdapter.createGetter(subscribable)
151
+ this._signals.set(attrName, signal)
152
+ }
153
+ const value = signal()
154
+ return value ?? (defaultValue as ApplogValue | undefined) ?? value
155
+ },
156
+ set(this: ViewModel, v: ApplogValue) {
157
+ const applog: ApplogForInsertOptionalAgent = { en: this.en, at: atPath, vl: v }
158
+ persistFn(this.thread, [applog])
159
+ },
160
+ enumerable: true,
161
+ configurable: true,
162
+ })
163
+ }
164
+
165
+ // ── Applog tracking: applogs where vl === this.en ────────────
166
+ const targets = new Map<EntityID, Set<string>>()
167
+
168
+ this._targetApplogs = rollingFilter(currentThread, { vl: this.en })
169
+ this._targetMap = new SubscribableImpl(
170
+ targets,
171
+ () => this._targetApplogs.subscribe((event) => {
172
+ if (isInitEvent(event)) {
173
+ targets.clear()
174
+ for (const log of event.init) {
175
+ let ats = targets.get(log.en)
176
+ if (!ats) {
177
+ ats = new Set()
178
+ targets.set(log.en, ats)
179
+ }
180
+ ats.add(log.at)
181
+ }
182
+ } else {
183
+ for (const log of event.added) {
184
+ let ats = targets.get(log.en)
185
+ if (!ats) {
186
+ ats = new Set()
187
+ targets.set(log.en, ats)
188
+ }
189
+ ats.add(log.at)
190
+ }
191
+ if (event.removed) {
192
+ for (const log of event.removed) {
193
+ const ats = targets.get(log.en)
194
+ if (ats) {
195
+ ats.delete(log.at)
196
+ if (ats.size === 0) targets.delete(log.en)
197
+ }
198
+ }
199
+ }
200
+ }
201
+ this._targetMap._set(targets)
202
+ }, 'derived'),
203
+ )
204
+ }
205
+
206
+ /** Thread scoped to this entity */
207
+ get entityThread(): Thread {
208
+ return rollingFilter(this.thread, { en: this.en })
209
+ }
210
+
211
+ /** Applogs in the current thread where vl === this.en */
212
+ get targetApplogs(): Thread {
213
+ return this._targetApplogs
214
+ }
215
+
216
+ /** Set of entities that have an applog with this.en as the vl */
217
+ get targettedBy(): Set<EntityID> {
218
+ return new Set(this._targetMap.value.keys())
219
+ }
220
+
221
+ /** Set of at strings from applogs where vl === this.en */
222
+ get targettedVia(): Set<string> {
223
+ const via = new Set<string>()
224
+ for (const ats of this._targetMap.value.values()) {
225
+ for (const at of ats) via.add(at)
226
+ }
227
+ return via
228
+ }
229
+
230
+ /** Whether this entity is soft-deleted */
231
+ get isDeleted(): boolean {
232
+ let signal = this._signals.get('__isDeleted')
233
+ if (!signal) {
234
+ const currentThread = makeCurrent(this.thread, options.makeCurrentThread)
235
+ const subscribable = liveEntityAt(currentThread, this.en, 'isDeleted')
236
+ signal = this._signalAdapter.createGetter(subscribable)
237
+ this._signals.set('__isDeleted', signal)
238
+ }
239
+ return !!signal()
240
+ }
241
+
242
+ /** Soft-delete this entity */
243
+ setDeleted(thread = this.thread): void {
244
+ const applog: ApplogForInsertOptionalAgent = { en: this.en, at: 'isDeleted', vl: true }
245
+ persistFn(thread, [applog])
246
+ }
247
+
248
+ /** Get a builder for updating this entity */
249
+ buildUpdate(init: Partial<T> = {}): ObjectBuilder<T> {
250
+ return new ObjectBuilder<T>(init, this.en, entityPrefix)
251
+ }
252
+
253
+ /** Description for debugging */
254
+ get description(): string {
255
+ return `${vmName}VM(en=${this.en})`
256
+ }
257
+
258
+ /** Get the full entity state as a plain object */
259
+ toJSON(): Partial<T> {
260
+ const result: Record<string, ApplogValue> = {}
261
+ for (const attr of attrDefs) {
262
+ const value = (this as any)[attr.name]
263
+ if (value !== undefined && value !== null) {
264
+ result[attr.name] = value
265
+ }
266
+ }
267
+ return result as Partial<T>
268
+ }
269
+ }
270
+
271
+ // ── Static Methods ────────────────────────────────────────────
272
+
273
+ /**
274
+ * Get or create a VM instance for the given entity and thread.
275
+ * Implements the singleton pattern — returns cached instance if one exists.
276
+ */
277
+ function getVMClass(en: EntityID, thread: Thread): InstanceType<typeof ViewModel> {
278
+ if (!en || typeof en !== 'string') throw new Error(`[${vmName}VM.get] invalid en: ${en}`)
279
+ if (!thread) throw new Error(`[${vmName}VM.get] no thread provided`)
280
+
281
+ const threadMap = getInstancesForThread(thread)
282
+ let entityMap = threadMap.get(vmName) as VMInstanceMap<InstanceType<typeof ViewModel>> | undefined
283
+ if (!entityMap) {
284
+ entityMap = new Map()
285
+ threadMap.set(vmName, entityMap)
286
+ }
287
+
288
+ const existing = entityMap.get(en)
289
+ if (existing) return existing
290
+
291
+ const vm = new ViewModel(en, thread, false) as InstanceType<typeof ViewModel>
292
+ entityMap.set(en, vm)
293
+ return vm
294
+ }
295
+
296
+ /**
297
+ * Create a builder for constructing a new entity.
298
+ */
299
+ function buildNewEntity(init: Partial<T> = {}, en?: EntityID): ObjectBuilder<T> {
300
+ const entityId = en ?? genId(init as any)
301
+ return ObjectBuilder.create<T>(init, entityId, entityPrefix)
302
+ }
303
+
304
+ // Attach static methods to the class
305
+ const VMClass: any = ViewModel
306
+ VMClass.get = getVMClass
307
+ VMClass.buildNew = buildNewEntity
308
+ VMClass.vmName = vmName
309
+ VMClass.entityPrefix = entityPrefix
310
+
311
+ return VMClass as unknown as {
312
+ new(
313
+ en: EntityID,
314
+ thread: Thread,
315
+ skipInit?: boolean,
316
+ ): IVMInstance<T> & T
317
+
318
+ /** Get or create a VM instance for the given entity ID */
319
+ get(en: EntityID, thread: Thread): IVMInstance<T> & T
320
+
321
+ /** Create a builder for a new entity */
322
+ buildNew(init?: Partial<T>, en?: EntityID): ObjectBuilder<T>
323
+
324
+ /** The VM name (for debugging) */
325
+ readonly vmName: string
326
+
327
+ /** The entity prefix used for at-paths */
328
+ readonly entityPrefix: string
329
+ }
330
+ }
@@ -0,0 +1,22 @@
1
+ // Core ViewModel Abstraction
2
+ // Framework-agnostic ViewModel factory system for wovin threads
3
+
4
+ export { ObjectBuilder } from './builder.ts'
5
+ export { createViewModelFactory, DefaultSignalAdapter } from './factory.ts'
6
+ export { createAdapterFromAttributes, buildAtPath } from './schema-adapter.ts'
7
+ export type {
8
+ ISchemaAdapter,
9
+ ISignalAdapter,
10
+ IVMInstance,
11
+ IVMClass,
12
+ VMAttributeDef,
13
+ ViewModelFactoryOptions,
14
+ VMInstanceMap,
15
+ } from './types.ts'
16
+ export { GlobalVMInstances, getInstancesForThread } from './types.ts'
17
+
18
+ // Schema Adapters
19
+ export { createTypeBoxAdapter } from './adapters/typebox.ts'
20
+ export { createZodAdapter } from './adapters/zod.ts'
21
+ export { createArkTypeAdapter } from './adapters/arktype.ts'
22
+ export { createTypiaAdapter } from './adapters/typia.ts'
@@ -0,0 +1,27 @@
1
+ import type { ApplogValue } from '../applog/datom-types.ts'
2
+ import type { ISchemaAdapter, VMAttributeDef } from './types.ts'
3
+
4
+ /**
5
+ * Helper to create a simple schema adapter from a plain attribute list.
6
+ * Useful for quick VM definitions or when you don't have a formal schema library.
7
+ */
8
+ export function createAdapterFromAttributes<T extends Record<string, ApplogValue>>(
9
+ config: {
10
+ attributes: VMAttributeDef[]
11
+ entityPrefix: string
12
+ defaults?: Partial<T>
13
+ },
14
+ ): ISchemaAdapter<T> {
15
+ return {
16
+ getAttributeDefs: () => config.attributes,
17
+ getDefaults: () => (config.defaults ?? {}) as Partial<T>,
18
+ getEntityPrefix: () => config.entityPrefix,
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Helper to build at-paths from attribute names and an entity prefix.
24
+ */
25
+ export function buildAtPath(entityPrefix: string, attrName: string): string {
26
+ return `${entityPrefix}/${attrName}`
27
+ }