@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,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
|
+
}
|