@wovin/core 0.3.0 → 0.3.3
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.js +5 -3
- package/dist/{chunk-2OXLPZQI.js → chunk-4MKPGQIM.js} +8 -16
- package/dist/chunk-4MKPGQIM.js.map +1 -0
- package/dist/{chunk-VGIACGWX.js → chunk-6CSJTSQP.js} +3 -3
- package/dist/{chunk-WVW4YXB5.js → chunk-BIYQEX3N.js} +2 -2
- package/dist/{chunk-Q4EMPWA3.js → chunk-H3JNNTVP.js} +9 -3
- package/dist/chunk-H3JNNTVP.js.map +1 -0
- package/dist/{chunk-EHO2BFFY.js → chunk-H4YVJKB7.js} +2 -2
- package/dist/chunk-N5QPZNKD.js +288 -0
- package/dist/chunk-N5QPZNKD.js.map +1 -0
- package/dist/{chunk-2PJFLZRC.js → chunk-N7SEGHU4.js} +2 -2
- package/dist/index.js +20 -10
- package/dist/ipfs/car.d.ts.map +1 -1
- package/dist/ipfs.js +4 -4
- package/dist/ipns/ipns-record.d.ts +19 -1
- package/dist/ipns/ipns-record.d.ts.map +1 -1
- package/dist/ipns/ipns-w3name.d.ts +31 -1
- package/dist/ipns/ipns-w3name.d.ts.map +1 -1
- package/dist/ipns/ipns-watcher.d.ts.map +1 -1
- package/dist/ipns.js +118 -21
- package/dist/ipns.js.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/index.js +4 -4
- package/dist/viewmodel/index.js.map +1 -1
- package/package.json +1 -1
- package/src/applog/applog-utils.test.ts +43 -1
- package/src/applog/applog-utils.ts +38 -21
- package/src/applog/object-values.test.ts +9 -9
- package/src/ipfs/car.ts +6 -0
- package/src/ipns/ipns-record.ts +95 -17
- package/src/ipns/ipns-w3name.ts +109 -5
- package/src/ipns/ipns-watcher.ts +4 -3
- 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/factory.ts +2 -2
- package/dist/chunk-2OXLPZQI.js.map +0 -1
- package/dist/chunk-OKXRRWNS.js +0 -138
- package/dist/chunk-OKXRRWNS.js.map +0 -1
- package/dist/chunk-Q4EMPWA3.js.map +0 -1
- /package/dist/{chunk-VGIACGWX.js.map → chunk-6CSJTSQP.js.map} +0 -0
- /package/dist/{chunk-WVW4YXB5.js.map → chunk-BIYQEX3N.js.map} +0 -0
- /package/dist/{chunk-EHO2BFFY.js.map → chunk-H4YVJKB7.js.map} +0 -0
- /package/dist/{chunk-2PJFLZRC.js.map → chunk-N7SEGHU4.js.map} +0 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import type { Applog, Attribute, Timestamp } from '../applog/datom-types.ts';
|
|
2
|
+
import type { Thread } from '../thread/basic.ts';
|
|
3
|
+
import type { WriteableThread } from '../thread/writeable.ts';
|
|
4
|
+
/**
|
|
5
|
+
* An EpochSnapshot maps every {@link Attribute} present in a time window to the
|
|
6
|
+
* array of {@link Applog}s (from any entity) that carry that attribute.
|
|
7
|
+
*
|
|
8
|
+
* This is **not** a flat array — logs are grouped by attribute first so that
|
|
9
|
+
* consumers can answer questions like "which entities had their `movie/title`
|
|
10
|
+
* updated in this window?" without re-filtering.
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* ```typescript
|
|
14
|
+
* const snap = createEpochSnapshot(thread, {
|
|
15
|
+
* start: '2024-01-01T00:00:00.000Z',
|
|
16
|
+
* end: '2024-06-01T00:00:00.000Z',
|
|
17
|
+
* })
|
|
18
|
+
* for (const [attr, logs] of snap) {
|
|
19
|
+
* console.log(`${attr}: ${logs.length} logs`)
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export type EpochSnapshot = ReadonlyMap<Attribute, readonly Applog[]>;
|
|
24
|
+
/**
|
|
25
|
+
* Options for {@link createEpochSnapshot}.
|
|
26
|
+
*/
|
|
27
|
+
export interface EpochSnapshotOptions {
|
|
28
|
+
/**
|
|
29
|
+
* Start of the time window (inclusive). ISO 8601 string matching the
|
|
30
|
+
* {@link Timestamp} format used on every {@link Applog}.
|
|
31
|
+
*/
|
|
32
|
+
readonly start: Timestamp;
|
|
33
|
+
/**
|
|
34
|
+
* End of the time window (exclusive). ISO 8601 string.
|
|
35
|
+
* Logs whose `ts` is **equal to** `end` are excluded.
|
|
36
|
+
*/
|
|
37
|
+
readonly end: Timestamp;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Creates an {@link EpochSnapshot} — a point-in-time grouping of all app logs
|
|
41
|
+
* within `[start, end)` grouped by their attribute name.
|
|
42
|
+
*
|
|
43
|
+
* The function is a **one-off (snapshot) computation**: it reads the current
|
|
44
|
+
* state of the thread and returns a frozen map. If you need reactivity, wrap
|
|
45
|
+
* the result in a reactive derivation or subscribe to thread changes and
|
|
46
|
+
* re-compute.
|
|
47
|
+
*
|
|
48
|
+
* @param thread - The thread to snapshot.
|
|
49
|
+
* @param options - Time range bounds.
|
|
50
|
+
* @returns A `ReadonlyMap` keyed by attribute name. Each value is the
|
|
51
|
+
* (chronologically sorted) array of applogs whose `at` matches that
|
|
52
|
+
* key and whose `ts` falls inside `[start, end)`.
|
|
53
|
+
*
|
|
54
|
+
* @throws {Error} If `start` is lexically `>=` `end` (empty range).
|
|
55
|
+
*
|
|
56
|
+
* @example
|
|
57
|
+
* ```typescript
|
|
58
|
+
* const snap = createEpochSnapshot(thread, {
|
|
59
|
+
* start: '2024-01-01T00:00:00.000Z',
|
|
60
|
+
* end: '2025-01-01T00:00:00.000Z',
|
|
61
|
+
* })
|
|
62
|
+
* // Iterate attribute groups:
|
|
63
|
+
* for (const [attr, logs] of snap) {
|
|
64
|
+
* console.log(attr, logs.length)
|
|
65
|
+
* }
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
export declare function createEpochSnapshot(thread: Thread, options: EpochSnapshotOptions): EpochSnapshot;
|
|
69
|
+
/**
|
|
70
|
+
* A flexible time specification used by {@link purgeHistory}.
|
|
71
|
+
*
|
|
72
|
+
* - `-1` (number) — the earliest log's timestamp in the thread ("beginning")
|
|
73
|
+
* - `0` (number) — current time ("now")
|
|
74
|
+
* - Positive number — treated as a Unix millisecond timestamp
|
|
75
|
+
* - `'-1d'`, `'-2h'`, `'-30m'`, `'-5s'` — relative offset **before** now
|
|
76
|
+
* - `'+1d'`, `'2h'` — relative offset **after** now (no sign = forward)
|
|
77
|
+
* - ISO 8601 string — absolute timestamp, used as-is
|
|
78
|
+
* - `'now'` — current time
|
|
79
|
+
*/
|
|
80
|
+
export type TimeSpec = number | string;
|
|
81
|
+
/**
|
|
82
|
+
* Resolve a {@link TimeSpec} to an absolute ISO {@link Timestamp}.
|
|
83
|
+
*
|
|
84
|
+
* @param spec - The time specification to resolve.
|
|
85
|
+
* @param thread - The thread (used to find earliest log when spec is `-1`).
|
|
86
|
+
* @param now - The reference "now" for relative specs.
|
|
87
|
+
* @returns An ISO 8601 timestamp string.
|
|
88
|
+
*/
|
|
89
|
+
export declare function resolveTimeSpec(spec: TimeSpec, thread: Thread, now: Date): Timestamp;
|
|
90
|
+
/**
|
|
91
|
+
* A JSON-serializable representation of an {@link EpochSnapshot}, safe for
|
|
92
|
+
* writing to disk, sending over the wire, or storing in IPFS.
|
|
93
|
+
*/
|
|
94
|
+
export interface SerializedEpochSnapshot {
|
|
95
|
+
/** Format marker for versioning */
|
|
96
|
+
readonly format: 'wovin-epoch-snapshot-v1';
|
|
97
|
+
/** The attribute whose history was captured / purged */
|
|
98
|
+
readonly attribute: string;
|
|
99
|
+
/** The time range this snapshot covers */
|
|
100
|
+
readonly timeRange: {
|
|
101
|
+
readonly start: Timestamp;
|
|
102
|
+
readonly end: Timestamp;
|
|
103
|
+
};
|
|
104
|
+
/** When this snapshot was created */
|
|
105
|
+
readonly createdAt: Timestamp;
|
|
106
|
+
/** Total number of applogs across all groups */
|
|
107
|
+
readonly logCount: number;
|
|
108
|
+
/**
|
|
109
|
+
* The grouped applogs. Each key is an attribute name; the value is the
|
|
110
|
+
* array of applogs that carried that attribute within the time window.
|
|
111
|
+
*/
|
|
112
|
+
readonly groups: Record<string, readonly Applog[]>;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Converts an {@link EpochSnapshot} into a {@link SerializedEpochSnapshot}
|
|
116
|
+
* that can be persisted as JSON.
|
|
117
|
+
*
|
|
118
|
+
* @param snapshot - The live snapshot to serialize.
|
|
119
|
+
* @param metadata - Identifying metadata to attach.
|
|
120
|
+
* @returns A plain, JSON-safe object.
|
|
121
|
+
*/
|
|
122
|
+
export declare function serializeEpochSnapshot(snapshot: EpochSnapshot, metadata: {
|
|
123
|
+
/** The attribute this purge targets */
|
|
124
|
+
readonly attribute: string;
|
|
125
|
+
/** The time range of the snapshot */
|
|
126
|
+
readonly timeRange: {
|
|
127
|
+
readonly start: Timestamp;
|
|
128
|
+
readonly end: Timestamp;
|
|
129
|
+
};
|
|
130
|
+
}): SerializedEpochSnapshot;
|
|
131
|
+
/**
|
|
132
|
+
* Options for {@link purgeHistory}.
|
|
133
|
+
*/
|
|
134
|
+
export interface PurgeHistoryOptions {
|
|
135
|
+
/**
|
|
136
|
+
* Callback invoked with the serialized snapshot **before** any logs are
|
|
137
|
+
* purged. Use this to persist the snapshot to a file, database, IPFS, or
|
|
138
|
+
* any other storage you choose.
|
|
139
|
+
*
|
|
140
|
+
* If omitted, the snapshot is created and returned in the result but not
|
|
141
|
+
* automatically persisted anywhere.
|
|
142
|
+
*/
|
|
143
|
+
persistSnapshot?: (serialized: SerializedEpochSnapshot) => Promise<void> | void;
|
|
144
|
+
/**
|
|
145
|
+
* Override "now" for deterministic tests. Defaults to `new Date()`.
|
|
146
|
+
*/
|
|
147
|
+
now?: Date;
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* The result of a successful {@link purgeHistory} call.
|
|
151
|
+
*/
|
|
152
|
+
export interface PurgeHistoryResult {
|
|
153
|
+
/**
|
|
154
|
+
* The {@link EpochSnapshot} that was taken **before** the purge.
|
|
155
|
+
* Contains all logs (across all attributes) within the time window, even
|
|
156
|
+
* though only one attribute was purged.
|
|
157
|
+
*/
|
|
158
|
+
readonly snapshot: EpochSnapshot;
|
|
159
|
+
/** Number of applogs that were removed from the thread. */
|
|
160
|
+
readonly purgedCount: number;
|
|
161
|
+
/** Number of applogs kept as LWW winners for the purged attribute. */
|
|
162
|
+
readonly keptCount: number;
|
|
163
|
+
/** The resolved time range that was processed. */
|
|
164
|
+
readonly timeRange: {
|
|
165
|
+
readonly start: Timestamp;
|
|
166
|
+
readonly end: Timestamp;
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* For a given **attribute** and **time range**:
|
|
171
|
+
*
|
|
172
|
+
* 1. Creates an {@link EpochSnapshot} capturing all app logs within
|
|
173
|
+
* `[start, end)` across **all** attributes.
|
|
174
|
+
* 2. Calls `options.persistSnapshot` with a serialized copy of the snapshot
|
|
175
|
+
* (so you can store the pre-purge state before any data is removed).
|
|
176
|
+
* 3. For the target attribute, groups the captured logs by entity and keeps
|
|
177
|
+
* only the **latest** (last-write-wins) applog per `(en, at)` pair.
|
|
178
|
+
* All older logs for that pair are purged from the thread.
|
|
179
|
+
*
|
|
180
|
+
* Logs for **other** attributes are never touched.
|
|
181
|
+
*
|
|
182
|
+
* @param thread - A writable thread to purge from.
|
|
183
|
+
* @param attribute - Only logs carrying this attribute are candidates for
|
|
184
|
+
* removal. Logs with other attributes are left untouched.
|
|
185
|
+
* @param start - Start of the time window (see {@link TimeSpec}).
|
|
186
|
+
* @param end - End of the time window, exclusive (see {@link TimeSpec}).
|
|
187
|
+
* @param options - Optional persistence callback and time override.
|
|
188
|
+
* @returns A {@link PurgeHistoryResult} with the snapshot and counts.
|
|
189
|
+
*
|
|
190
|
+
* @throws {RangeError} If the resolved `start` is not before `end`.
|
|
191
|
+
*
|
|
192
|
+
* @example
|
|
193
|
+
* ```typescript
|
|
194
|
+
* // Purge all but the latest block/content per entity from the beginning
|
|
195
|
+
* // up to yesterday, saving a snapshot first.
|
|
196
|
+
* const result = await purgeHistory(thread, 'block/content', -1, '-1d', {
|
|
197
|
+
* persistSnapshot: async (snap) => {
|
|
198
|
+
* await writeFile('purge-archive.json', JSON.stringify(snap, null, 2))
|
|
199
|
+
* },
|
|
200
|
+
* })
|
|
201
|
+
* console.log(`Purged ${result.purgedCount}, kept ${result.keptCount}`)
|
|
202
|
+
* ```
|
|
203
|
+
*/
|
|
204
|
+
export declare function purgeHistory(thread: WriteableThread, attribute: Attribute, start: TimeSpec, end: TimeSpec, options?: PurgeHistoryOptions): Promise<PurgeHistoryResult>;
|
|
205
|
+
//# sourceMappingURL=epoch-snapshot.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"epoch-snapshot.d.ts","sourceRoot":"","sources":["../../src/query/epoch-snapshot.ts"],"names":[],"mappings":"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"}
|
package/dist/query.d.ts
CHANGED
package/dist/query.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,yBAAyB,CAAA;AACvC,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,8BAA8B,CAAA"}
|
|
1
|
+
{"version":3,"file":"query.d.ts","sourceRoot":"","sources":["../src/query.ts"],"names":[],"mappings":"AAAA,cAAc,uBAAuB,CAAA;AACrC,cAAc,kBAAkB,CAAA;AAChC,cAAc,wBAAwB,CAAA;AACtC,cAAc,qBAAqB,CAAA;AACnC,cAAc,qBAAqB,CAAA;AACnC,cAAc,yBAAyB,CAAA;AACvC,cAAc,kBAAkB,CAAA;AAChC,cAAc,yBAAyB,CAAA;AACvC,cAAc,8BAA8B,CAAA;AAC5C,cAAc,2BAA2B,CAAA"}
|
package/dist/query.js
CHANGED
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
|
+
createEpochSnapshot,
|
|
2
3
|
liveEntityCollection,
|
|
3
|
-
|
|
4
|
-
|
|
4
|
+
purgeHistory,
|
|
5
|
+
queryDivergencesByPrev,
|
|
6
|
+
resolveTimeSpec,
|
|
7
|
+
serializeEpochSnapshot
|
|
8
|
+
} from "./chunk-N5QPZNKD.js";
|
|
5
9
|
import {
|
|
6
10
|
LiveQueryResult,
|
|
7
11
|
QueryNode,
|
|
@@ -44,7 +48,7 @@ import {
|
|
|
44
48
|
throwOnTimeout,
|
|
45
49
|
withTimeout,
|
|
46
50
|
withoutDeleted
|
|
47
|
-
} from "./chunk-
|
|
51
|
+
} from "./chunk-N7SEGHU4.js";
|
|
48
52
|
import {
|
|
49
53
|
SubscribableArrayImpl,
|
|
50
54
|
SubscribableImpl,
|
|
@@ -57,7 +61,7 @@ import {
|
|
|
57
61
|
memoizedFn,
|
|
58
62
|
prettifyThreadName,
|
|
59
63
|
refCountedMemoizedFn
|
|
60
|
-
} from "./chunk-
|
|
64
|
+
} from "./chunk-4MKPGQIM.js";
|
|
61
65
|
import "./chunk-ZAADLBSB.js";
|
|
62
66
|
export {
|
|
63
67
|
LiveQueryResult,
|
|
@@ -69,6 +73,7 @@ export {
|
|
|
69
73
|
anyOf,
|
|
70
74
|
createDebugName,
|
|
71
75
|
createDebugNameObj,
|
|
76
|
+
createEpochSnapshot,
|
|
72
77
|
createObjMapper,
|
|
73
78
|
entityOverlap,
|
|
74
79
|
entityOverlapCount,
|
|
@@ -98,6 +103,7 @@ export {
|
|
|
98
103
|
prefixAt,
|
|
99
104
|
prefixAttrs,
|
|
100
105
|
prettifyThreadName,
|
|
106
|
+
purgeHistory,
|
|
101
107
|
query,
|
|
102
108
|
queryAndMap,
|
|
103
109
|
queryDivergencesByPrev,
|
|
@@ -108,6 +114,8 @@ export {
|
|
|
108
114
|
queryStepOnce,
|
|
109
115
|
refCountedMemoizedFn,
|
|
110
116
|
resolveKeyMapper,
|
|
117
|
+
resolveTimeSpec,
|
|
118
|
+
serializeEpochSnapshot,
|
|
111
119
|
startsWith,
|
|
112
120
|
stripAtPrefix,
|
|
113
121
|
threadFromMaybeArray,
|
package/dist/retrieve.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
updateThreadFromSnapshot,
|
|
3
3
|
withBlockCache
|
|
4
|
-
} from "./chunk-
|
|
5
|
-
import "./chunk-
|
|
4
|
+
} from "./chunk-6CSJTSQP.js";
|
|
5
|
+
import "./chunk-H3JNNTVP.js";
|
|
6
6
|
import "./chunk-YDAKBU6Q.js";
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-N7SEGHU4.js";
|
|
8
|
+
import "./chunk-4MKPGQIM.js";
|
|
9
9
|
import "./chunk-ZAADLBSB.js";
|
|
10
10
|
export {
|
|
11
11
|
updateThreadFromSnapshot,
|
package/dist/thread.js
CHANGED
package/dist/viewmodel/index.js
CHANGED
|
@@ -13,16 +13,16 @@ import {
|
|
|
13
13
|
import {
|
|
14
14
|
lastWriteWins,
|
|
15
15
|
liveEntityAt
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-N7SEGHU4.js";
|
|
17
17
|
import {
|
|
18
18
|
EntityID_LENGTH,
|
|
19
19
|
SubscribableImpl,
|
|
20
20
|
dateNowIso,
|
|
21
21
|
ensureTsPvAndFinalizeApplog,
|
|
22
|
-
getHashID,
|
|
23
22
|
isInitEvent,
|
|
23
|
+
mintNewEntity,
|
|
24
24
|
rollingFilter
|
|
25
|
-
} from "../chunk-
|
|
25
|
+
} from "../chunk-4MKPGQIM.js";
|
|
26
26
|
import "../chunk-ZAADLBSB.js";
|
|
27
27
|
|
|
28
28
|
// src/viewmodel/builder.ts
|
|
@@ -122,7 +122,7 @@ function createViewModelFactory(options) {
|
|
|
122
122
|
} = options;
|
|
123
123
|
const attrDefs = adapter.getAttributeDefs();
|
|
124
124
|
const defaults = adapter.getDefaults();
|
|
125
|
-
const genId = generateEntityId ?? ((data) =>
|
|
125
|
+
const genId = generateEntityId ?? ((data) => mintNewEntity({ ...data, ts: data.ts ?? dateNowIso() }, entityIdLength));
|
|
126
126
|
const persistFn = (thread, applogs) => {
|
|
127
127
|
defaultPersistApplogs(thread, applogs);
|
|
128
128
|
};
|
|
@@ -1 +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":[]}
|
|
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, mintNewEntity } 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\tmintNewEntity({ ...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":[]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { describe, expect, it } from 'vitest'
|
|
2
2
|
import type { Applog } from './datom-types.ts'
|
|
3
|
-
import { compareApplogsByTs, isLaterByTsAndPv, sortApplogsByTs } from './applog-utils.ts'
|
|
3
|
+
import { compareApplogsByTs, deriveDeterministicEntityID, isLaterByTsAndPv, mintNewEntity, sortApplogsByTs } from './applog-utils.ts'
|
|
4
4
|
|
|
5
5
|
const mkLog = (overrides: Partial<Applog>): Applog => ({
|
|
6
6
|
cid: 'cid-default' as any,
|
|
@@ -106,3 +106,45 @@ describe('isLaterByTsAndPv', () => {
|
|
|
106
106
|
expect(isLaterByTsAndPv(b, a)).toBe('other'.localeCompare('shared') > 0)
|
|
107
107
|
})
|
|
108
108
|
})
|
|
109
|
+
|
|
110
|
+
describe('deriveDeterministicEntityID — natural-key identity', () => {
|
|
111
|
+
it('is deterministic: same key ⇒ same id (write & read converge on one soul)', () => {
|
|
112
|
+
const a = deriveDeterministicEntityID({ type: 'segment', mediaId: 'm1', startMs: 0, endMs: 100 })
|
|
113
|
+
const b = deriveDeterministicEntityID({ type: 'segment', mediaId: 'm1', startMs: 0, endMs: 100 })
|
|
114
|
+
expect(a).toBe(b)
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
it('is stable across key property order (safe-stable-stringify)', () => {
|
|
118
|
+
const a = deriveDeterministicEntityID({ mediaId: 'm1', type: 'segment' })
|
|
119
|
+
const b = deriveDeterministicEntityID({ type: 'segment', mediaId: 'm1' })
|
|
120
|
+
expect(a).toBe(b)
|
|
121
|
+
})
|
|
122
|
+
|
|
123
|
+
it('different keys ⇒ different ids', () => {
|
|
124
|
+
expect(deriveDeterministicEntityID('#rust')).not.toBe(deriveDeterministicEntityID('#typescript'))
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
it('honors the requested length', () => {
|
|
128
|
+
expect(deriveDeterministicEntityID('#rust', 7)).toHaveLength(7)
|
|
129
|
+
expect(deriveDeterministicEntityID('#rust', 10)).toHaveLength(10)
|
|
130
|
+
})
|
|
131
|
+
})
|
|
132
|
+
|
|
133
|
+
describe('mintNewEntity — occurrence identity', () => {
|
|
134
|
+
it('re-minting the same logical seed with a fresh ts yields a DIFFERENT id (the re-mint trap)', () => {
|
|
135
|
+
// Documents why an existing entity must be resolved & its `en` reused rather than
|
|
136
|
+
// re-minted: a timestamp in the seed forks a new soul on every call.
|
|
137
|
+
const first = mintNewEntity({ msgId: 42, ts: '2026-06-16T00:00:00.000Z' })
|
|
138
|
+
const editedLater = mintNewEntity({ msgId: 42, ts: '2026-06-16T00:00:01.000Z' })
|
|
139
|
+
expect(first).not.toBe(editedLater)
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('is deterministic for an identical seed (same ts ⇒ same id)', () => {
|
|
143
|
+
const seed = { msgId: 42, ts: '2026-06-16T00:00:00.000Z' }
|
|
144
|
+
expect(mintNewEntity(seed)).toBe(mintNewEntity({ ...seed }))
|
|
145
|
+
})
|
|
146
|
+
|
|
147
|
+
it('honors the requested length', () => {
|
|
148
|
+
expect(mintNewEntity({ a: 1 }, 6)).toHaveLength(6)
|
|
149
|
+
})
|
|
150
|
+
})
|
|
@@ -255,7 +255,35 @@ export const removeDuplicateAppLogs = (appLogArray: Applog[], mode?: RemoveDupli
|
|
|
255
255
|
// return Array.from(logMap.values())
|
|
256
256
|
// }
|
|
257
257
|
|
|
258
|
-
|
|
258
|
+
/** Low-level: stable-stringify + cyrb53 → hex entity id of `lngth` chars. Prefer the named wrappers below. */
|
|
259
|
+
const hashToEntityId = (stringifiable: any, lngth = 8) => cyrb53hash(stringify(stringifiable), 31, lngth) as string
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Mint a brand-new entity id — coin a fresh "soul" for a genuinely new occurrence.
|
|
263
|
+
*
|
|
264
|
+
* The hashed `seed` MUST include something that makes this occurrence unique (a timestamp
|
|
265
|
+
* and/or random nonce), otherwise two distinct creations collide on one id.
|
|
266
|
+
*
|
|
267
|
+
* ⚠️ NEVER call this to re-reference an entity that already exists: doing so mints a *new*
|
|
268
|
+
* soul and forks a duplicate (breaking pv / lastWriteWins chains). To reference an existing
|
|
269
|
+
* entity, look it up and reuse its `en`. For identity that is *defined by* a stable key, use
|
|
270
|
+
* {@link deriveDeterministicEntityID}. See docs/architecture.md#entity-identity-soul.
|
|
271
|
+
*/
|
|
272
|
+
export const mintNewEntity = (seed: any, lngth = 8) => hashToEntityId(seed, lngth)
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Derive a deterministic entity id from a stable, identity-defining key.
|
|
276
|
+
*
|
|
277
|
+
* Same key ⇒ same `en`, so all references converge on one soul — used for both writing and
|
|
278
|
+
* reading the same entity (singleton / natural-key entities, e.g. a tag, a per-agent setting,
|
|
279
|
+
* a media segment).
|
|
280
|
+
*
|
|
281
|
+
* ⚠️ Use with care: two users/agents using the same name/key for something does NOT necessarily
|
|
282
|
+
* mean they mean the same thing/entity/soul. Only use when the key TRULY defines identity. The
|
|
283
|
+
* key must be canonicalized — diverging formats fork the soul and silently break lookups.
|
|
284
|
+
* See docs/architecture.md#entity-identity-soul.
|
|
285
|
+
*/
|
|
286
|
+
export const deriveDeterministicEntityID = (stableKey: any, lngth = 8) => hashToEntityId(stableKey, lngth)
|
|
259
287
|
|
|
260
288
|
export function isVariable(x: any): x is string {
|
|
261
289
|
return typeof x === 'string' && x.startsWith('?')
|
|
@@ -324,21 +352,16 @@ export function matchPartStatic(field: keyof Applog, patternPart: ValueOrMatcher
|
|
|
324
352
|
result = patternPart === atomPart // shortcut for most common use-case
|
|
325
353
|
} else if (typ === 'function') {
|
|
326
354
|
result = (patternPart as Function)(atomPart)
|
|
327
|
-
} else if (
|
|
328
|
-
// A
|
|
329
|
-
//
|
|
330
|
-
|
|
331
|
-
`[matchPartStatic] a bare array is not a valid matcher for field '${field}'.`
|
|
332
|
-
+ ` Use anyOf(...) for set-membership, or a predicate (v) => isEqual(v, [...]) to match a literal array value.`,
|
|
333
|
-
patternPart,
|
|
334
|
-
)
|
|
335
|
-
} else if (typeof (patternPart as any).has === 'function') {
|
|
336
|
-
result = (patternPart as Set<any>).has(atomPart)
|
|
355
|
+
} else if (patternPart instanceof Set) {
|
|
356
|
+
// A Set means set-membership (use anyOf(...) to build one). Bare arrays/objects are
|
|
357
|
+
// NOT membership — they are literal values compared by deep equality (see else).
|
|
358
|
+
result = patternPart.has(atomPart)
|
|
337
359
|
} // if (field === 'at' && typ === 'string' && patternPart.endsWith('*')) {
|
|
338
360
|
// return typeof atomPart === 'string' && atomPart.startsWith(patternPart.slice(0, -1))
|
|
339
361
|
// }
|
|
340
362
|
else {
|
|
341
|
-
// object/array values compare by deep equality;
|
|
363
|
+
// object/array values (and primitives) compare by deep equality;
|
|
364
|
+
// isEqual short-circuits on === so primitives stay on the cheap path
|
|
342
365
|
result = valueEq(patternPart as ApplogValue, atomPart)
|
|
343
366
|
}
|
|
344
367
|
} else {
|
|
@@ -368,15 +391,9 @@ export function matchPart(patternPart: ValueOrMatcher<ApplogValue>, atomPart: Ap
|
|
|
368
391
|
if (typeof patternPart === 'function') {
|
|
369
392
|
return patternPart(atomPart) ? context : null
|
|
370
393
|
}
|
|
371
|
-
if (
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
+ ` Use anyOf(...) for set-membership, or a predicate to match a literal array value.`,
|
|
375
|
-
patternPart,
|
|
376
|
-
)
|
|
377
|
-
}
|
|
378
|
-
if (patternPart && typeof (patternPart as any).has === 'function') {
|
|
379
|
-
return (patternPart as Set<any>).has(atomPart) ? context : null
|
|
394
|
+
if (patternPart instanceof Set) {
|
|
395
|
+
// set-membership (use anyOf(...)); bare arrays/objects are literal deep-equality matches below
|
|
396
|
+
return patternPart.has(atomPart) ? context : null
|
|
380
397
|
}
|
|
381
398
|
return valueEq(patternPart as ApplogValue, atomPart) ? context : null
|
|
382
399
|
}
|