@wovin/core 0.1.36 → 0.2.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/README.md +0 -12
- package/dist/applog/applog-helpers.d.ts +12 -12
- package/dist/applog/applog-helpers.d.ts.map +1 -1
- package/dist/applog/applog-utils.d.ts +40 -6
- package/dist/applog/applog-utils.d.ts.map +1 -1
- package/dist/applog/datom-types.d.ts +67 -12
- package/dist/applog/datom-types.d.ts.map +1 -1
- package/dist/applog.d.ts +3 -3
- package/dist/applog.d.ts.map +1 -1
- package/dist/{applog.min.js → applog.js} +12 -7
- package/dist/blockstore.d.ts +1 -1
- package/dist/blockstore.d.ts.map +1 -1
- package/dist/{blockstore.min.js → blockstore.js} +1 -3
- package/dist/{blockstore.min.js.map → blockstore.js.map} +1 -1
- package/dist/chunk-22WDFLXO.js +138 -0
- package/dist/chunk-22WDFLXO.js.map +1 -0
- package/dist/chunk-3SUFNJEZ.js +1026 -0
- package/dist/chunk-3SUFNJEZ.js.map +1 -0
- package/dist/chunk-6ALNRM3J.js +435 -0
- package/dist/chunk-6ALNRM3J.js.map +1 -0
- package/dist/chunk-7Z5YDQKK.js +1 -0
- package/dist/{chunk-KXMTKPF4.min.js → chunk-BLF5MAWU.js} +8 -8
- package/dist/chunk-BLF5MAWU.js.map +1 -0
- package/dist/chunk-E46VTKTZ.js +1 -0
- package/dist/{chunk-H3VQJP56.min.js → chunk-HUIQ54TT.js} +9 -9
- package/dist/chunk-HUIQ54TT.js.map +1 -0
- package/dist/{chunk-BRC7LSM6.min.js → chunk-OC6Z6CQW.js} +5 -5
- package/dist/chunk-OC6Z6CQW.js.map +1 -0
- package/dist/chunk-SHUHRHOT.js +1923 -0
- package/dist/chunk-SHUHRHOT.js.map +1 -0
- package/dist/{chunk-QPGEBDMJ.min.js → chunk-YDAKBU6Q.js} +1 -1
- package/dist/chunk-YDAKBU6Q.js.map +1 -0
- package/dist/chunk-ZAADLBSB.js +36 -0
- package/dist/chunk-ZAADLBSB.js.map +1 -0
- package/dist/index.d.ts +7 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/{index.min.js → index.js} +81 -46
- package/dist/ipfs/car.d.ts +11 -11
- package/dist/ipfs/car.d.ts.map +1 -1
- package/dist/ipfs/ipfs-utils.d.ts +2 -2
- package/dist/ipfs/ipfs-utils.d.ts.map +1 -1
- package/dist/ipfs.d.ts +3 -3
- package/dist/ipfs.d.ts.map +1 -1
- package/dist/{ipfs.min.js → ipfs.js} +7 -10
- package/dist/ipns.d.ts +1 -1
- package/dist/ipns.d.ts.map +1 -1
- package/dist/ipns.js +64 -0
- package/dist/ipns.js.map +1 -0
- package/dist/pubsub/pub-pull.d.ts +3 -3
- package/dist/pubsub/pub-pull.d.ts.map +1 -1
- package/dist/pubsub/pubsub-types.d.ts +3 -3
- package/dist/pubsub/pubsub-types.d.ts.map +1 -1
- package/dist/pubsub/snap-push.d.ts +4 -4
- package/dist/pubsub/snap-push.d.ts.map +1 -1
- package/dist/pubsub/ucan.d.ts +1 -1
- package/dist/pubsub/ucan.d.ts.map +1 -1
- package/dist/pubsub.d.ts +4 -4
- package/dist/pubsub.d.ts.map +1 -1
- package/dist/{pubsub.min.js → pubsub.js} +7 -10
- package/dist/query/attr-helpers.d.ts +5 -0
- package/dist/query/attr-helpers.d.ts.map +1 -0
- package/dist/query/basic.d.ts +87 -23
- package/dist/query/basic.d.ts.map +1 -1
- package/dist/query/divergences.d.ts +5 -5
- package/dist/query/divergences.d.ts.map +1 -1
- package/dist/query/entity-collection.d.ts +19 -0
- package/dist/query/entity-collection.d.ts.map +1 -0
- package/dist/query/matchers.d.ts +12 -1
- package/dist/query/matchers.d.ts.map +1 -1
- package/dist/query/memoized.d.ts +66 -0
- package/dist/query/memoized.d.ts.map +1 -0
- package/dist/query/situations.d.ts +2 -1
- package/dist/query/situations.d.ts.map +1 -1
- package/dist/query/subscribable.d.ts +111 -0
- package/dist/query/subscribable.d.ts.map +1 -0
- package/dist/query/types.d.ts +54 -14
- package/dist/query/types.d.ts.map +1 -1
- package/dist/query.d.ts +9 -5
- package/dist/query.d.ts.map +1 -1
- package/dist/{query.min.js → query.js} +55 -34
- package/dist/retrieve/index.d.ts +1 -1
- package/dist/retrieve/index.d.ts.map +1 -1
- package/dist/retrieve/update-thread.d.ts +3 -3
- package/dist/retrieve/update-thread.d.ts.map +1 -1
- package/dist/retrieve.d.ts +1 -1
- package/dist/retrieve.d.ts.map +1 -1
- package/dist/retrieve.js +14 -0
- package/dist/thread/basic.d.ts +15 -19
- package/dist/thread/basic.d.ts.map +1 -1
- package/dist/thread/filters.d.ts +8 -10
- package/dist/thread/filters.d.ts.map +1 -1
- package/dist/thread/indexes.d.ts +57 -0
- package/dist/thread/indexes.d.ts.map +1 -0
- package/dist/thread/mapped.d.ts +40 -11
- package/dist/thread/mapped.d.ts.map +1 -1
- package/dist/thread/utils.d.ts +5 -5
- package/dist/thread/utils.d.ts.map +1 -1
- package/dist/thread/writeable.d.ts +2 -2
- package/dist/thread/writeable.d.ts.map +1 -1
- package/dist/thread.d.ts +6 -5
- package/dist/thread.d.ts.map +1 -1
- package/dist/{thread.min.js → thread.js} +9 -6
- package/dist/types/typescript-utils.d.ts +6 -5
- package/dist/types/typescript-utils.d.ts.map +1 -1
- package/dist/types.d.ts +1 -1
- package/dist/types.d.ts.map +1 -1
- package/dist/{types.min.js → types.js} +3 -4
- package/dist/utils/debug-name.d.ts +13 -0
- package/dist/utils/debug-name.d.ts.map +1 -0
- package/dist/utils.d.ts +1 -1
- package/dist/utils.d.ts.map +1 -1
- package/dist/utils.js +9 -0
- package/package.json +32 -23
- package/src/applog/applog-helpers.ts +155 -0
- package/src/applog/applog-utils.test.ts +108 -0
- package/src/applog/applog-utils.ts +551 -0
- package/src/applog/datom-types.ts +167 -0
- package/src/applog/object-values.test.ts +106 -0
- package/src/applog.ts +3 -0
- package/src/blockstore/index.ts +36 -0
- package/src/blockstore.ts +1 -0
- package/src/index.ts +8 -0
- package/src/ipfs/car.ts +291 -0
- package/src/ipfs/fetch-snapshot-chain.ts +135 -0
- package/src/ipfs/ipfs-utils.ts +132 -0
- package/src/ipfs.ts +3 -0
- package/src/ipns/ipns-record.ts +115 -0
- package/src/ipns.ts +1 -0
- package/src/pubsub/UCAN Specs Overview.md +217 -0
- package/src/pubsub/connector.ts +9 -0
- package/src/pubsub/pub-pull.ts +31 -0
- package/src/pubsub/pubsub-types.ts +90 -0
- package/src/pubsub/snap-push.ts +278 -0
- package/src/pubsub/ucan-example.ts +61 -0
- package/src/pubsub/ucan.ts +56 -0
- package/src/pubsub.ts +4 -0
- package/src/query/attr-helpers.ts +5 -0
- package/src/query/basic.ts +1245 -0
- package/src/query/divergences.ts +50 -0
- package/src/query/entity-collection.ts +132 -0
- package/src/query/liveFilterAndMap.test.ts +102 -0
- package/src/query/matchers.ts +30 -0
- package/src/query/memoized.test.ts +151 -0
- package/src/query/memoized.ts +180 -0
- package/src/query/query-steps.ts +4 -0
- package/src/query/query.test.ts +538 -0
- package/src/query/situations.ts +261 -0
- package/src/query/subscribable.test.ts +245 -0
- package/src/query/subscribable.ts +234 -0
- package/src/query/types.ts +155 -0
- package/src/query/withoutDeleted.test.ts +204 -0
- package/src/query.ts +9 -0
- package/src/retrieve/index.ts +1 -0
- package/src/retrieve/update-thread.ts +248 -0
- package/src/retrieve.ts +1 -0
- package/src/test/perf/query.1m.perf.test.ts +94 -0
- package/src/test/perf/query.perf.test.ts +389 -0
- package/src/test/perf/query.realdata.perf.test.ts +182 -0
- package/src/thread/basic.ts +209 -0
- package/src/thread/filters.ts +227 -0
- package/src/thread/indexes.ts +256 -0
- package/src/thread/joinThreads.test.ts +304 -0
- package/src/thread/mapped.ts +226 -0
- package/src/thread/utils.ts +144 -0
- package/src/thread/writeable.ts +163 -0
- package/src/thread.ts +6 -0
- package/src/types/typescript-utils.ts +64 -0
- package/src/types.ts +1 -0
- package/src/utils/debug-name.ts +54 -0
- package/src/utils.ts +4 -0
- package/dist/chunk-2Y2PYHGR.min.js +0 -65
- package/dist/chunk-2Y2PYHGR.min.js.map +0 -1
- package/dist/chunk-5MMGBK2U.min.js +0 -1
- package/dist/chunk-7IDQIMQO.min.js +0 -1
- package/dist/chunk-BRC7LSM6.min.js.map +0 -1
- package/dist/chunk-COXXILXC.min.js +0 -512
- package/dist/chunk-COXXILXC.min.js.map +0 -1
- package/dist/chunk-GDX2OO7L.min.js +0 -9080
- package/dist/chunk-GDX2OO7L.min.js.map +0 -1
- package/dist/chunk-H3VQJP56.min.js.map +0 -1
- package/dist/chunk-HYMC7W6S.min.js +0 -1549
- package/dist/chunk-HYMC7W6S.min.js.map +0 -1
- package/dist/chunk-KEHU7HGZ.min.js +0 -5216
- package/dist/chunk-KEHU7HGZ.min.js.map +0 -1
- package/dist/chunk-KXMTKPF4.min.js.map +0 -1
- package/dist/chunk-PHITDXZT.min.js +0 -36
- package/dist/chunk-QO2KMGDN.min.js +0 -3771
- package/dist/chunk-QO2KMGDN.min.js.map +0 -1
- package/dist/chunk-QPGEBDMJ.min.js.map +0 -1
- package/dist/chunk-WXLCBTHX.min.js +0 -1606
- package/dist/chunk-WXLCBTHX.min.js.map +0 -1
- package/dist/ipns.min.js +0 -6419
- package/dist/ipns.min.js.map +0 -1
- package/dist/mobx/mobx-utils.d.ts +0 -82
- package/dist/mobx/mobx-utils.d.ts.map +0 -1
- package/dist/mobx.d.ts +0 -2
- package/dist/mobx.d.ts.map +0 -1
- package/dist/mobx.min.js +0 -141
- package/dist/retrieve.min.js +0 -17
- package/dist/types.min.js.map +0 -1
- package/dist/utils.min.js +0 -10
- package/dist/utils.min.js.map +0 -1
- /package/dist/{applog.min.js.map → applog.js.map} +0 -0
- /package/dist/{chunk-5MMGBK2U.min.js.map → chunk-7Z5YDQKK.js.map} +0 -0
- /package/dist/{chunk-7IDQIMQO.min.js.map → chunk-E46VTKTZ.js.map} +0 -0
- /package/dist/{chunk-PHITDXZT.min.js.map → index.js.map} +0 -0
- /package/dist/{index.min.js.map → ipfs.js.map} +0 -0
- /package/dist/{ipfs.min.js.map → pubsub.js.map} +0 -0
- /package/dist/{mobx.min.js.map → query.js.map} +0 -0
- /package/dist/{pubsub.min.js.map → retrieve.js.map} +0 -0
- /package/dist/{query.min.js.map → thread.js.map} +0 -0
- /package/dist/{retrieve.min.js.map → types.js.map} +0 -0
- /package/dist/{thread.min.js.map → utils.js.map} +0 -0
|
@@ -0,0 +1,261 @@
|
|
|
1
|
+
import { Applog } from '../applog/datom-types.ts'
|
|
2
|
+
import { query } from './basic.ts'
|
|
3
|
+
import { QueryNode } from './types.ts'
|
|
4
|
+
import { Logger } from 'besonders-logger'
|
|
5
|
+
|
|
6
|
+
const { WARN, LOG, DEBUG, VERBOSE, ERROR } = Logger.setup(Logger.INFO) // eslint-disable-line no-unused-vars
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Situations are meant to flag and possibly autocorrect or autosuggest solutions
|
|
10
|
+
*
|
|
11
|
+
* Types of Situations:
|
|
12
|
+
* Divergences = leaf nodes that are based on different previous versions/contexts
|
|
13
|
+
* Conflicts/Disagreement = different agents setting the same attribute (based on same previous)
|
|
14
|
+
* Overwrite = one agent's action implicitly overrides another's without explicit confirmation
|
|
15
|
+
* Conscious Disagreement = agents explicitly acknowledge and accept conflicting changes
|
|
16
|
+
* Suggestions = recommendations for improvements or alternatives
|
|
17
|
+
*/
|
|
18
|
+
interface Situation {
|
|
19
|
+
name: string
|
|
20
|
+
desc: string
|
|
21
|
+
intensity: number
|
|
22
|
+
type: typeof SituationTypes[keyof typeof SituationTypes]
|
|
23
|
+
query: typeof query // wovin can export common examples
|
|
24
|
+
resolutionOptions: Resolution[]
|
|
25
|
+
metadata?: {
|
|
26
|
+
agentId?: string
|
|
27
|
+
timestamp?: number
|
|
28
|
+
context?: Record<string, any>
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface QueryResultWithSituations {
|
|
33
|
+
result: any
|
|
34
|
+
situations: Situation[]
|
|
35
|
+
threadView?: ThreadView
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
interface ThreadView {
|
|
39
|
+
id: string
|
|
40
|
+
situations: SituatedMessage[]
|
|
41
|
+
resolutionState: ResolutionState
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
interface SituatedMessage {
|
|
45
|
+
id: string
|
|
46
|
+
situation: Situation
|
|
47
|
+
message: string
|
|
48
|
+
resolutions: ResolutionAction[]
|
|
49
|
+
timestamp: number
|
|
50
|
+
agentId?: string
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
interface ResolutionState {
|
|
54
|
+
pending: string[] // situation IDs
|
|
55
|
+
resolved: string[] // situation IDs
|
|
56
|
+
ignored: string[] // situation IDs
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
interface ResolutionAction {
|
|
60
|
+
name: string
|
|
61
|
+
applied: boolean
|
|
62
|
+
appliedAt?: number
|
|
63
|
+
appliedBy?: string
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const SituationTypes = {
|
|
67
|
+
divergence: 'divergence',
|
|
68
|
+
disagreement: 'disagreement',
|
|
69
|
+
overwrite: 'overwrite',
|
|
70
|
+
consciousDisagreement: 'consciousDisagreement',
|
|
71
|
+
suggestion: 'suggestion',
|
|
72
|
+
} as const
|
|
73
|
+
interface Resolution {
|
|
74
|
+
name: string
|
|
75
|
+
desc: string
|
|
76
|
+
assertion: () => Applog[]
|
|
77
|
+
}
|
|
78
|
+
const ContentDivergence = {
|
|
79
|
+
name: 'ContentDivergence',
|
|
80
|
+
desc: 'block/content changes from different agents based on different pv logs',
|
|
81
|
+
intensity: 10,
|
|
82
|
+
type: SituationTypes.divergence,
|
|
83
|
+
query: QueryNode,
|
|
84
|
+
resolutionOptions: [],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const AttributeOverwrite = {
|
|
88
|
+
name: 'AttributeOverwrite',
|
|
89
|
+
desc: 'one agent implicitly overwrites another agent\'s attribute change without confirmation',
|
|
90
|
+
intensity: 8,
|
|
91
|
+
type: SituationTypes.overwrite,
|
|
92
|
+
query: QueryNode,
|
|
93
|
+
resolutionOptions: [
|
|
94
|
+
{
|
|
95
|
+
name: 'restore',
|
|
96
|
+
desc: 'restore the previous value',
|
|
97
|
+
assertion: () => [] // TODO: implement restoration logic
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
name: 'confirm',
|
|
101
|
+
desc: 'confirm the overwrite as intentional',
|
|
102
|
+
assertion: () => [] // TODO: implement confirmation logic
|
|
103
|
+
}
|
|
104
|
+
],
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const ConsciousDisagreement = {
|
|
108
|
+
name: 'ConsciousDisagreement',
|
|
109
|
+
desc: 'agents explicitly acknowledge and accept conflicting changes to the same attribute',
|
|
110
|
+
intensity: 5,
|
|
111
|
+
type: SituationTypes.consciousDisagreement,
|
|
112
|
+
query: QueryNode,
|
|
113
|
+
resolutionOptions: [
|
|
114
|
+
{
|
|
115
|
+
name: 'merge',
|
|
116
|
+
desc: 'create a merged version that incorporates both perspectives',
|
|
117
|
+
assertion: () => [] // TODO: implement merge logic
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
name: 'branch',
|
|
121
|
+
desc: 'create separate branches for each perspective',
|
|
122
|
+
assertion: () => [] // TODO: implement branching logic
|
|
123
|
+
}
|
|
124
|
+
],
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const ContentSuggestion = {
|
|
128
|
+
name: 'ContentSuggestion',
|
|
129
|
+
desc: 'AI or agent suggests improvements to existing content',
|
|
130
|
+
intensity: 3,
|
|
131
|
+
type: SituationTypes.suggestion,
|
|
132
|
+
query: QueryNode,
|
|
133
|
+
resolutionOptions: [
|
|
134
|
+
{
|
|
135
|
+
name: 'accept',
|
|
136
|
+
desc: 'accept the suggestion and apply changes',
|
|
137
|
+
assertion: () => [] // TODO: implement acceptance logic
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
name: 'reject',
|
|
141
|
+
desc: 'reject the suggestion',
|
|
142
|
+
assertion: () => [] // TODO: implement rejection logic
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
name: 'modify',
|
|
146
|
+
desc: 'accept with modifications',
|
|
147
|
+
assertion: () => [] // TODO: implement modification logic
|
|
148
|
+
}
|
|
149
|
+
],
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Creates a thread view for situations detected in query results
|
|
154
|
+
*/
|
|
155
|
+
export function createThreadForSituations(
|
|
156
|
+
result: any,
|
|
157
|
+
situations: Situation[],
|
|
158
|
+
options: {
|
|
159
|
+
threadId?: string
|
|
160
|
+
groupByType?: boolean
|
|
161
|
+
sortByIntensity?: boolean
|
|
162
|
+
} = {}
|
|
163
|
+
): QueryResultWithSituations {
|
|
164
|
+
const threadId = options.threadId || `thread_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`
|
|
165
|
+
|
|
166
|
+
// Sort situations if requested
|
|
167
|
+
let sortedSituations = [...situations]
|
|
168
|
+
if (options.sortByIntensity) {
|
|
169
|
+
sortedSituations.sort((a, b) => b.intensity - a.intensity)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Group by type if requested
|
|
173
|
+
const threadMessages: SituatedMessage[] = sortedSituations.map((situation, index) => ({
|
|
174
|
+
id: `${threadId}_msg_${index}`,
|
|
175
|
+
situation,
|
|
176
|
+
message: formatSituationMessage(situation),
|
|
177
|
+
resolutions: situation.resolutionOptions.map(option => ({
|
|
178
|
+
name: option.name,
|
|
179
|
+
applied: false,
|
|
180
|
+
})),
|
|
181
|
+
timestamp: Date.now(),
|
|
182
|
+
agentId: situation.metadata?.agentId,
|
|
183
|
+
}))
|
|
184
|
+
|
|
185
|
+
const threadView: ThreadView = {
|
|
186
|
+
id: threadId,
|
|
187
|
+
situations: threadMessages,
|
|
188
|
+
resolutionState: {
|
|
189
|
+
pending: threadMessages.map(msg => msg.id),
|
|
190
|
+
resolved: [],
|
|
191
|
+
ignored: [],
|
|
192
|
+
},
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return {
|
|
196
|
+
result,
|
|
197
|
+
situations,
|
|
198
|
+
threadView,
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Formats a situation into a human-readable message for the thread
|
|
204
|
+
*/
|
|
205
|
+
function formatSituationMessage(situation: Situation): string {
|
|
206
|
+
const typeEmoji = {
|
|
207
|
+
[SituationTypes.divergence]: '🔀',
|
|
208
|
+
[SituationTypes.disagreement]: '⚠️',
|
|
209
|
+
[SituationTypes.overwrite]: '🔄',
|
|
210
|
+
[SituationTypes.consciousDisagreement]: '💭',
|
|
211
|
+
[SituationTypes.suggestion]: '💡',
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const emoji = typeEmoji[situation.type] || '❓'
|
|
215
|
+
return `${emoji} **${situation.name}**: ${situation.desc} (intensity: ${situation.intensity})`
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
* Applies a resolution action to a situation in a thread
|
|
220
|
+
*/
|
|
221
|
+
export function applyResolution(
|
|
222
|
+
threadView: ThreadView,
|
|
223
|
+
messageId: string,
|
|
224
|
+
resolutionName: string,
|
|
225
|
+
appliedBy: string
|
|
226
|
+
): ThreadView {
|
|
227
|
+
const updatedMessages = threadView.situations.map(msg => {
|
|
228
|
+
if (msg.id === messageId) {
|
|
229
|
+
const updatedResolutions = msg.resolutions.map(res =>
|
|
230
|
+
res.name === resolutionName
|
|
231
|
+
? { ...res, applied: true, appliedAt: Date.now(), appliedBy }
|
|
232
|
+
: res
|
|
233
|
+
)
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
...msg,
|
|
237
|
+
resolutions: updatedResolutions,
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return msg
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
const message = updatedMessages.find(msg => msg.id === messageId)
|
|
244
|
+
const hasAppliedResolution = message?.resolutions.some(res => res.applied)
|
|
245
|
+
|
|
246
|
+
const resolutionState = {
|
|
247
|
+
...threadView.resolutionState,
|
|
248
|
+
pending: hasAppliedResolution
|
|
249
|
+
? threadView.resolutionState.pending.filter(id => id !== messageId)
|
|
250
|
+
: threadView.resolutionState.pending,
|
|
251
|
+
resolved: hasAppliedResolution
|
|
252
|
+
? [...threadView.resolutionState.resolved, messageId]
|
|
253
|
+
: threadView.resolutionState.resolved,
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return {
|
|
257
|
+
...threadView,
|
|
258
|
+
situations: updatedMessages,
|
|
259
|
+
resolutionState,
|
|
260
|
+
}
|
|
261
|
+
}
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest'
|
|
2
|
+
import { ArrayEvent, isArrayInitEvent, SubscribableArrayImpl, SubscribableImpl } from './subscribable.ts'
|
|
3
|
+
|
|
4
|
+
describe('SubscribableImpl', () => {
|
|
5
|
+
it('provides value synchronously', () => {
|
|
6
|
+
const s = new SubscribableImpl(42)
|
|
7
|
+
expect(s.value).toBe(42)
|
|
8
|
+
})
|
|
9
|
+
|
|
10
|
+
it('does NOT fire callback on subscribe', () => {
|
|
11
|
+
const s = new SubscribableImpl('hello')
|
|
12
|
+
const calls: string[] = []
|
|
13
|
+
s.subscribe(() => calls.push(s.value))
|
|
14
|
+
expect(calls).toEqual([])
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('notifies on _set', () => {
|
|
18
|
+
const s = new SubscribableImpl(1)
|
|
19
|
+
const values: number[] = []
|
|
20
|
+
s.subscribe(() => values.push(s.value))
|
|
21
|
+
s._set(2)
|
|
22
|
+
s._set(3)
|
|
23
|
+
expect(values).toEqual([2, 3])
|
|
24
|
+
expect(s.value).toBe(3)
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('activates upstream lazily on first subscribe', () => {
|
|
28
|
+
const deactivate = vi.fn()
|
|
29
|
+
const activate = vi.fn(() => deactivate)
|
|
30
|
+
const s = new SubscribableImpl(0, activate)
|
|
31
|
+
|
|
32
|
+
expect(activate).not.toHaveBeenCalled()
|
|
33
|
+
const unsub = s.subscribe(() => {})
|
|
34
|
+
expect(activate).toHaveBeenCalledOnce()
|
|
35
|
+
|
|
36
|
+
// Second subscriber doesn't re-activate
|
|
37
|
+
const unsub2 = s.subscribe(() => {})
|
|
38
|
+
expect(activate).toHaveBeenCalledOnce()
|
|
39
|
+
|
|
40
|
+
// Unsubscribe one — upstream stays active
|
|
41
|
+
unsub2()
|
|
42
|
+
expect(deactivate).not.toHaveBeenCalled()
|
|
43
|
+
|
|
44
|
+
// Unsubscribe last — upstream deactivates
|
|
45
|
+
unsub()
|
|
46
|
+
expect(deactivate).toHaveBeenCalledOnce()
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
it('re-activates upstream after full unsubscribe + re-subscribe', () => {
|
|
50
|
+
const deactivate = vi.fn()
|
|
51
|
+
const activate = vi.fn(() => deactivate)
|
|
52
|
+
const s = new SubscribableImpl(0, activate)
|
|
53
|
+
|
|
54
|
+
const unsub1 = s.subscribe(() => {})
|
|
55
|
+
unsub1()
|
|
56
|
+
expect(activate).toHaveBeenCalledTimes(1)
|
|
57
|
+
expect(deactivate).toHaveBeenCalledTimes(1)
|
|
58
|
+
|
|
59
|
+
const unsub2 = s.subscribe(() => {})
|
|
60
|
+
expect(activate).toHaveBeenCalledTimes(2)
|
|
61
|
+
unsub2()
|
|
62
|
+
expect(deactivate).toHaveBeenCalledTimes(2)
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
it('snapshots subscribers to handle unsubscribe during notification', () => {
|
|
66
|
+
const s = new SubscribableImpl(0)
|
|
67
|
+
const events: string[] = []
|
|
68
|
+
let unsub2: (() => void) | null = null
|
|
69
|
+
|
|
70
|
+
s.subscribe(() => {
|
|
71
|
+
events.push('sub1')
|
|
72
|
+
unsub2?.()
|
|
73
|
+
})
|
|
74
|
+
unsub2 = s.subscribe(() => {
|
|
75
|
+
events.push('sub2')
|
|
76
|
+
})
|
|
77
|
+
|
|
78
|
+
// No immediate callbacks — nothing fired yet
|
|
79
|
+
expect(events).toEqual([])
|
|
80
|
+
|
|
81
|
+
// _set — sub1 runs and unsubscribes sub2, but sub2 still fires (snapshot)
|
|
82
|
+
s._set(1)
|
|
83
|
+
expect(events).toEqual(['sub1', 'sub2'])
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
it('dispose tears down upstream and clears subscribers', () => {
|
|
87
|
+
const deactivate = vi.fn()
|
|
88
|
+
const activate = vi.fn(() => deactivate)
|
|
89
|
+
const s = new SubscribableImpl(0, activate)
|
|
90
|
+
|
|
91
|
+
s.subscribe(() => {})
|
|
92
|
+
expect(activate).toHaveBeenCalledOnce()
|
|
93
|
+
|
|
94
|
+
s.dispose()
|
|
95
|
+
expect(deactivate).toHaveBeenCalledOnce()
|
|
96
|
+
|
|
97
|
+
// _set after dispose — no error, no subscribers
|
|
98
|
+
s._set(99)
|
|
99
|
+
expect(s.value).toBe(99)
|
|
100
|
+
})
|
|
101
|
+
|
|
102
|
+
it('works with complex types', () => {
|
|
103
|
+
const s = new SubscribableImpl<{ name: string } | null>(null)
|
|
104
|
+
const values: ({ name: string } | null)[] = []
|
|
105
|
+
s.subscribe(() => values.push(s.value))
|
|
106
|
+
s._set({ name: 'Alice' })
|
|
107
|
+
s._set(null)
|
|
108
|
+
expect(values).toEqual([{ name: 'Alice' }, null])
|
|
109
|
+
})
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
describe('SubscribableArrayImpl', () => {
|
|
113
|
+
it('provides items synchronously', () => {
|
|
114
|
+
const arr = new SubscribableArrayImpl([1, 2, 3])
|
|
115
|
+
expect(arr.items).toEqual([1, 2, 3])
|
|
116
|
+
expect(arr.length).toBe(3)
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
it('does NOT send init event on subscribe', () => {
|
|
120
|
+
const arr = new SubscribableArrayImpl([1, 2, 3])
|
|
121
|
+
const events: ArrayEvent<number>[] = []
|
|
122
|
+
arr.subscribe(e => events.push(e))
|
|
123
|
+
expect(events).toEqual([])
|
|
124
|
+
// Current state is available via .items
|
|
125
|
+
expect(arr.items).toEqual([1, 2, 3])
|
|
126
|
+
})
|
|
127
|
+
|
|
128
|
+
it('notifies on _push', () => {
|
|
129
|
+
const arr = new SubscribableArrayImpl([1])
|
|
130
|
+
const events: ArrayEvent<number>[] = []
|
|
131
|
+
arr.subscribe(e => events.push(e))
|
|
132
|
+
arr._push(2, 3)
|
|
133
|
+
expect(arr.items).toEqual([1, 2, 3])
|
|
134
|
+
expect(events).toEqual([
|
|
135
|
+
{ added: [2, 3], removed: null },
|
|
136
|
+
])
|
|
137
|
+
})
|
|
138
|
+
|
|
139
|
+
it('notifies on _remove', () => {
|
|
140
|
+
const arr = new SubscribableArrayImpl([1, 2, 3])
|
|
141
|
+
const events: ArrayEvent<number>[] = []
|
|
142
|
+
arr.subscribe(e => events.push(e))
|
|
143
|
+
arr._remove([2])
|
|
144
|
+
expect(arr.items).toEqual([1, 3])
|
|
145
|
+
expect(events).toEqual([
|
|
146
|
+
{ added: [], removed: [2] },
|
|
147
|
+
])
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
it('notifies on _reset', () => {
|
|
151
|
+
const arr = new SubscribableArrayImpl([1, 2])
|
|
152
|
+
const events: ArrayEvent<number>[] = []
|
|
153
|
+
arr.subscribe(e => events.push(e))
|
|
154
|
+
arr._reset([10, 20])
|
|
155
|
+
expect(arr.items).toEqual([10, 20])
|
|
156
|
+
expect(events).toEqual([
|
|
157
|
+
{ init: [10, 20] },
|
|
158
|
+
])
|
|
159
|
+
})
|
|
160
|
+
|
|
161
|
+
it('activates upstream lazily on first subscribe', () => {
|
|
162
|
+
const deactivate = vi.fn()
|
|
163
|
+
const activate = vi.fn(() => deactivate)
|
|
164
|
+
const arr = new SubscribableArrayImpl([1], activate)
|
|
165
|
+
|
|
166
|
+
expect(activate).not.toHaveBeenCalled()
|
|
167
|
+
const unsub = arr.subscribe(() => {})
|
|
168
|
+
expect(activate).toHaveBeenCalledOnce()
|
|
169
|
+
|
|
170
|
+
// Second subscriber doesn't re-activate
|
|
171
|
+
const unsub2 = arr.subscribe(() => {})
|
|
172
|
+
expect(activate).toHaveBeenCalledOnce()
|
|
173
|
+
|
|
174
|
+
// Unsubscribe one — upstream stays active
|
|
175
|
+
unsub2()
|
|
176
|
+
expect(deactivate).not.toHaveBeenCalled()
|
|
177
|
+
|
|
178
|
+
// Unsubscribe last — upstream deactivates
|
|
179
|
+
unsub()
|
|
180
|
+
expect(deactivate).toHaveBeenCalledOnce()
|
|
181
|
+
})
|
|
182
|
+
|
|
183
|
+
it('re-activates upstream after full unsubscribe + re-subscribe', () => {
|
|
184
|
+
const deactivate = vi.fn()
|
|
185
|
+
const activate = vi.fn(() => deactivate)
|
|
186
|
+
const arr = new SubscribableArrayImpl([1], activate)
|
|
187
|
+
|
|
188
|
+
const unsub1 = arr.subscribe(() => {})
|
|
189
|
+
unsub1()
|
|
190
|
+
expect(activate).toHaveBeenCalledTimes(1)
|
|
191
|
+
expect(deactivate).toHaveBeenCalledTimes(1)
|
|
192
|
+
|
|
193
|
+
// Re-subscribe should re-activate
|
|
194
|
+
const unsub2 = arr.subscribe(() => {})
|
|
195
|
+
expect(activate).toHaveBeenCalledTimes(2)
|
|
196
|
+
unsub2()
|
|
197
|
+
expect(deactivate).toHaveBeenCalledTimes(2)
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
it('snapshots subscribers to handle unsubscribe during notification', () => {
|
|
201
|
+
const arr = new SubscribableArrayImpl<number>([])
|
|
202
|
+
const events: string[] = []
|
|
203
|
+
let unsub2: (() => void) | null = null
|
|
204
|
+
|
|
205
|
+
arr.subscribe(() => {
|
|
206
|
+
events.push('sub1')
|
|
207
|
+
// Unsubscribe sub2 during notification
|
|
208
|
+
unsub2?.()
|
|
209
|
+
})
|
|
210
|
+
unsub2 = arr.subscribe(() => {
|
|
211
|
+
events.push('sub2')
|
|
212
|
+
})
|
|
213
|
+
|
|
214
|
+
// No init events — nothing fired yet
|
|
215
|
+
expect(events).toEqual([])
|
|
216
|
+
|
|
217
|
+
// Push — sub1 runs and unsubscribes sub2, but sub2 should still receive this event
|
|
218
|
+
// (because we snapshot subscribers before iterating)
|
|
219
|
+
arr._push(1)
|
|
220
|
+
expect(events).toEqual(['sub1', 'sub2'])
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('dispose tears down upstream and clears subscribers', () => {
|
|
224
|
+
const deactivate = vi.fn()
|
|
225
|
+
const activate = vi.fn(() => deactivate)
|
|
226
|
+
const arr = new SubscribableArrayImpl([1], activate)
|
|
227
|
+
|
|
228
|
+
arr.subscribe(() => {})
|
|
229
|
+
expect(activate).toHaveBeenCalledOnce()
|
|
230
|
+
|
|
231
|
+
arr.dispose()
|
|
232
|
+
expect(deactivate).toHaveBeenCalledOnce()
|
|
233
|
+
|
|
234
|
+
// Push after dispose — no subscribers to notify (no error)
|
|
235
|
+
arr._push(2)
|
|
236
|
+
expect(arr.items).toEqual([1, 2])
|
|
237
|
+
})
|
|
238
|
+
})
|
|
239
|
+
|
|
240
|
+
describe('isArrayInitEvent', () => {
|
|
241
|
+
it('identifies init events', () => {
|
|
242
|
+
expect(isArrayInitEvent({ init: [1, 2] })).toBe(true)
|
|
243
|
+
expect(isArrayInitEvent({ added: [1], removed: null })).toBe(false)
|
|
244
|
+
})
|
|
245
|
+
})
|