mvc-kit 2.5.3 → 2.5.4
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/Channel.cjs +291 -0
- package/dist/Channel.cjs.map +1 -0
- package/dist/Channel.js +291 -0
- package/dist/Channel.js.map +1 -0
- package/dist/Collection.cjs +452 -0
- package/dist/Collection.cjs.map +1 -0
- package/dist/Collection.js +452 -0
- package/dist/Collection.js.map +1 -0
- package/dist/Controller.cjs +57 -0
- package/dist/Controller.cjs.map +1 -0
- package/dist/Controller.js +57 -0
- package/dist/Controller.js.map +1 -0
- package/dist/EventBus.cjs +84 -0
- package/dist/EventBus.cjs.map +1 -0
- package/dist/EventBus.js +84 -0
- package/dist/EventBus.js.map +1 -0
- package/dist/Model.cjs +175 -0
- package/dist/Model.cjs.map +1 -0
- package/dist/Model.js +175 -0
- package/dist/Model.js.map +1 -0
- package/dist/PersistentCollection.cjs +285 -0
- package/dist/PersistentCollection.cjs.map +1 -0
- package/dist/PersistentCollection.js +285 -0
- package/dist/PersistentCollection.js.map +1 -0
- package/dist/Resource.cjs +308 -0
- package/dist/Resource.cjs.map +1 -0
- package/dist/Resource.js +308 -0
- package/dist/Resource.js.map +1 -0
- package/dist/Service.cjs +51 -0
- package/dist/Service.cjs.map +1 -0
- package/dist/Service.js +51 -0
- package/dist/Service.js.map +1 -0
- package/dist/ViewModel.cjs +582 -0
- package/dist/ViewModel.cjs.map +1 -0
- package/dist/ViewModel.d.ts +1 -7
- package/dist/ViewModel.d.ts.map +1 -1
- package/dist/ViewModel.js +582 -0
- package/dist/ViewModel.js.map +1 -0
- package/dist/errors.cjs +79 -0
- package/dist/errors.cjs.map +1 -0
- package/dist/errors.js +79 -0
- package/dist/errors.js.map +1 -0
- package/dist/mvc-kit.cjs +29 -1
- package/dist/mvc-kit.cjs.map +1 -1
- package/dist/mvc-kit.js +27 -1132
- package/dist/mvc-kit.js.map +1 -1
- package/dist/react/guards.cjs +7 -0
- package/dist/react/guards.cjs.map +1 -0
- package/dist/react/guards.js +7 -0
- package/dist/react/guards.js.map +1 -0
- package/dist/react/provider.cjs +26 -0
- package/dist/react/provider.cjs.map +1 -0
- package/dist/react/provider.js +26 -0
- package/dist/react/provider.js.map +1 -0
- package/dist/react/use-event-bus.cjs +26 -0
- package/dist/react/use-event-bus.cjs.map +1 -0
- package/dist/react/use-event-bus.js +26 -0
- package/dist/react/use-event-bus.js.map +1 -0
- package/dist/react/use-instance.cjs +31 -0
- package/dist/react/use-instance.cjs.map +1 -0
- package/dist/react/use-instance.js +31 -0
- package/dist/react/use-instance.js.map +1 -0
- package/dist/react/use-local.cjs +64 -0
- package/dist/react/use-local.cjs.map +1 -0
- package/dist/react/use-local.js +64 -0
- package/dist/react/use-local.js.map +1 -0
- package/dist/react/use-model.cjs +80 -0
- package/dist/react/use-model.cjs.map +1 -0
- package/dist/react/use-model.js +80 -0
- package/dist/react/use-model.js.map +1 -0
- package/dist/react/use-singleton.cjs +21 -0
- package/dist/react/use-singleton.cjs.map +1 -0
- package/dist/react/use-singleton.js +21 -0
- package/dist/react/use-singleton.js.map +1 -0
- package/dist/react/use-teardown.cjs +22 -0
- package/dist/react/use-teardown.cjs.map +1 -0
- package/dist/react/use-teardown.js +22 -0
- package/dist/react/use-teardown.js.map +1 -0
- package/dist/react-native/NativeCollection.cjs +76 -0
- package/dist/react-native/NativeCollection.cjs.map +1 -0
- package/dist/react-native/NativeCollection.js +76 -0
- package/dist/react-native/NativeCollection.js.map +1 -0
- package/dist/react-native.cjs +4 -1
- package/dist/react-native.cjs.map +1 -1
- package/dist/react-native.js +2 -60
- package/dist/react-native.js.map +1 -1
- package/dist/react.cjs +19 -1
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +17 -145
- package/dist/react.js.map +1 -1
- package/dist/singleton.cjs +34 -0
- package/dist/singleton.cjs.map +1 -0
- package/dist/singleton.js +34 -0
- package/dist/singleton.js.map +1 -0
- package/dist/walkPrototypeChain.cjs +15 -0
- package/dist/walkPrototypeChain.cjs.map +1 -0
- package/dist/walkPrototypeChain.d.ts +9 -0
- package/dist/walkPrototypeChain.d.ts.map +1 -0
- package/dist/walkPrototypeChain.js +15 -0
- package/dist/walkPrototypeChain.js.map +1 -0
- package/dist/web/IndexedDBCollection.cjs +37 -0
- package/dist/web/IndexedDBCollection.cjs.map +1 -0
- package/dist/web/IndexedDBCollection.js +37 -0
- package/dist/web/IndexedDBCollection.js.map +1 -0
- package/dist/web/WebStorageCollection.cjs +85 -0
- package/dist/web/WebStorageCollection.cjs.map +1 -0
- package/dist/web/WebStorageCollection.js +85 -0
- package/dist/web/WebStorageCollection.js.map +1 -0
- package/dist/web/idb.cjs +121 -0
- package/dist/web/idb.cjs.map +1 -0
- package/dist/web/idb.js +121 -0
- package/dist/web/idb.js.map +1 -0
- package/dist/web.cjs +6 -1
- package/dist/web.cjs.map +1 -1
- package/dist/web.js +4 -178
- package/dist/web.js.map +1 -1
- package/package.json +4 -2
- package/dist/PersistentCollection-B8kNECDj.cjs +0 -2
- package/dist/PersistentCollection-B8kNECDj.cjs.map +0 -1
- package/dist/PersistentCollection-CbYqzFHc.js +0 -542
- package/dist/PersistentCollection-CbYqzFHc.js.map +0 -1
- package/dist/singleton-CaEXSbYg.js +0 -89
- package/dist/singleton-CaEXSbYg.js.map +0 -1
- package/dist/singleton-L-u2W_lX.cjs +0 -2
- package/dist/singleton-L-u2W_lX.cjs.map +0 -1
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
const __DEV__ = typeof __MVC_KIT_DEV__ !== "undefined" && __MVC_KIT_DEV__;
|
|
2
|
+
class Collection {
|
|
3
|
+
/** Maximum number of items before FIFO eviction. 0 = unlimited. */
|
|
4
|
+
static MAX_SIZE = 0;
|
|
5
|
+
/** Time-to-live in milliseconds. 0 = no expiry. */
|
|
6
|
+
static TTL = 0;
|
|
7
|
+
_items = [];
|
|
8
|
+
_disposed = false;
|
|
9
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
10
|
+
_index = /* @__PURE__ */ new Map();
|
|
11
|
+
_abortController = null;
|
|
12
|
+
_cleanups = null;
|
|
13
|
+
_timestamps = null;
|
|
14
|
+
_evictionTimer = null;
|
|
15
|
+
constructor(initialItems = []) {
|
|
16
|
+
let result = [...initialItems];
|
|
17
|
+
if (this._ttl > 0) {
|
|
18
|
+
this._timestamps = /* @__PURE__ */ new Map();
|
|
19
|
+
const now = Date.now();
|
|
20
|
+
for (const item of result) {
|
|
21
|
+
this._timestamps.set(item.id, now);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
if (this._maxSize > 0 && result.length > this._maxSize) {
|
|
25
|
+
const excess = result.length - this._maxSize;
|
|
26
|
+
const evicted = result.slice(0, excess);
|
|
27
|
+
result = result.slice(excess);
|
|
28
|
+
for (const item of evicted) {
|
|
29
|
+
this._timestamps?.delete(item.id);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
this._items = Object.freeze(result);
|
|
33
|
+
this.rebuildIndex();
|
|
34
|
+
this._scheduleEvictionTimer();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Alias for Subscribable compatibility.
|
|
38
|
+
*/
|
|
39
|
+
get state() {
|
|
40
|
+
return this._items;
|
|
41
|
+
}
|
|
42
|
+
/** The raw array of items. */
|
|
43
|
+
get items() {
|
|
44
|
+
return this._items;
|
|
45
|
+
}
|
|
46
|
+
/** Number of items in the collection. */
|
|
47
|
+
get length() {
|
|
48
|
+
return this._items.length;
|
|
49
|
+
}
|
|
50
|
+
/** Whether this instance has been disposed. */
|
|
51
|
+
get disposed() {
|
|
52
|
+
return this._disposed;
|
|
53
|
+
}
|
|
54
|
+
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
|
55
|
+
get disposeSignal() {
|
|
56
|
+
if (!this._abortController) {
|
|
57
|
+
this._abortController = new AbortController();
|
|
58
|
+
}
|
|
59
|
+
return this._abortController.signal;
|
|
60
|
+
}
|
|
61
|
+
// ── Config Accessors ──
|
|
62
|
+
get _maxSize() {
|
|
63
|
+
return this.constructor.MAX_SIZE;
|
|
64
|
+
}
|
|
65
|
+
get _ttl() {
|
|
66
|
+
return this.constructor.TTL;
|
|
67
|
+
}
|
|
68
|
+
// ── CRUD Methods (notify listeners) ──
|
|
69
|
+
/**
|
|
70
|
+
* Add one or more items. Items with existing IDs are silently skipped.
|
|
71
|
+
*/
|
|
72
|
+
add(...items) {
|
|
73
|
+
if (this._disposed) {
|
|
74
|
+
throw new Error("Cannot add to disposed Collection");
|
|
75
|
+
}
|
|
76
|
+
if (items.length === 0) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const seen = /* @__PURE__ */ new Set();
|
|
80
|
+
const newItems = [];
|
|
81
|
+
for (const item of items) {
|
|
82
|
+
if (!this._index.has(item.id) && !seen.has(item.id)) {
|
|
83
|
+
newItems.push(item);
|
|
84
|
+
seen.add(item.id);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (newItems.length === 0) return;
|
|
88
|
+
const prev = this._items;
|
|
89
|
+
let result = [...prev, ...newItems];
|
|
90
|
+
for (const item of newItems) {
|
|
91
|
+
this._index.set(item.id, item);
|
|
92
|
+
}
|
|
93
|
+
if (this._timestamps) {
|
|
94
|
+
const now = Date.now();
|
|
95
|
+
for (const item of newItems) {
|
|
96
|
+
this._timestamps.set(item.id, now);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (this._maxSize > 0 && result.length > this._maxSize) {
|
|
100
|
+
result = this._evictForCapacity(result);
|
|
101
|
+
}
|
|
102
|
+
this._items = Object.freeze(result);
|
|
103
|
+
this.notify(prev);
|
|
104
|
+
this._scheduleEvictionTimer();
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Add or replace items by ID. Existing items are replaced in-place
|
|
108
|
+
* (preserving array position); new items are appended. Deduplicates
|
|
109
|
+
* input — last occurrence wins. No-op if nothing changed (reference
|
|
110
|
+
* comparison).
|
|
111
|
+
*/
|
|
112
|
+
upsert(...items) {
|
|
113
|
+
if (this._disposed) {
|
|
114
|
+
throw new Error("Cannot upsert on disposed Collection");
|
|
115
|
+
}
|
|
116
|
+
if (items.length === 0) return;
|
|
117
|
+
const incoming = /* @__PURE__ */ new Map();
|
|
118
|
+
for (const item of items) {
|
|
119
|
+
incoming.set(item.id, item);
|
|
120
|
+
}
|
|
121
|
+
const prev = this._items;
|
|
122
|
+
let changed = false;
|
|
123
|
+
const replaced = /* @__PURE__ */ new Set();
|
|
124
|
+
const newArray = [];
|
|
125
|
+
for (const existing of prev) {
|
|
126
|
+
if (incoming.has(existing.id)) {
|
|
127
|
+
const replacement = incoming.get(existing.id);
|
|
128
|
+
if (replacement !== existing) changed = true;
|
|
129
|
+
newArray.push(replacement);
|
|
130
|
+
replaced.add(existing.id);
|
|
131
|
+
} else {
|
|
132
|
+
newArray.push(existing);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
for (const [id, item] of incoming) {
|
|
136
|
+
if (!replaced.has(id)) {
|
|
137
|
+
newArray.push(item);
|
|
138
|
+
changed = true;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (!changed) return;
|
|
142
|
+
if (this._timestamps) {
|
|
143
|
+
const now = Date.now();
|
|
144
|
+
for (const [id] of incoming) {
|
|
145
|
+
this._timestamps.set(id, now);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
for (const [id, item] of incoming) {
|
|
149
|
+
this._index.set(id, item);
|
|
150
|
+
}
|
|
151
|
+
let result = newArray;
|
|
152
|
+
if (this._maxSize > 0 && result.length > this._maxSize) {
|
|
153
|
+
result = this._evictForCapacity(result);
|
|
154
|
+
}
|
|
155
|
+
this._items = Object.freeze(result);
|
|
156
|
+
this.notify(prev);
|
|
157
|
+
this._scheduleEvictionTimer();
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Remove items by id(s).
|
|
161
|
+
*/
|
|
162
|
+
remove(...ids) {
|
|
163
|
+
if (this._disposed) {
|
|
164
|
+
throw new Error("Cannot remove from disposed Collection");
|
|
165
|
+
}
|
|
166
|
+
if (ids.length === 0) {
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
const idSet = new Set(ids);
|
|
170
|
+
const filtered = this._items.filter((item) => !idSet.has(item.id));
|
|
171
|
+
if (filtered.length === this._items.length) {
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
const prev = this._items;
|
|
175
|
+
this._items = Object.freeze(filtered);
|
|
176
|
+
for (const id of ids) {
|
|
177
|
+
this._index.delete(id);
|
|
178
|
+
this._timestamps?.delete(id);
|
|
179
|
+
}
|
|
180
|
+
this.notify(prev);
|
|
181
|
+
this._scheduleEvictionTimer();
|
|
182
|
+
}
|
|
183
|
+
/**
|
|
184
|
+
* Update an item by id with partial changes.
|
|
185
|
+
*/
|
|
186
|
+
update(id, changes) {
|
|
187
|
+
if (this._disposed) {
|
|
188
|
+
throw new Error("Cannot update disposed Collection");
|
|
189
|
+
}
|
|
190
|
+
const idx = this._items.findIndex((item) => item.id === id);
|
|
191
|
+
if (idx === -1) {
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
const existing = this._items[idx];
|
|
195
|
+
const updated = { ...existing, ...changes, id };
|
|
196
|
+
const keys = Object.keys(changes);
|
|
197
|
+
const hasChanges = keys.some((key) => changes[key] !== existing[key]);
|
|
198
|
+
if (!hasChanges) {
|
|
199
|
+
return;
|
|
200
|
+
}
|
|
201
|
+
const prev = this._items;
|
|
202
|
+
const newItems = [...prev];
|
|
203
|
+
newItems[idx] = updated;
|
|
204
|
+
this._items = Object.freeze(newItems);
|
|
205
|
+
this._index.set(id, updated);
|
|
206
|
+
this.notify(prev);
|
|
207
|
+
}
|
|
208
|
+
/**
|
|
209
|
+
* Replace all items.
|
|
210
|
+
*/
|
|
211
|
+
reset(items) {
|
|
212
|
+
if (this._disposed) {
|
|
213
|
+
throw new Error("Cannot reset disposed Collection");
|
|
214
|
+
}
|
|
215
|
+
const prev = this._items;
|
|
216
|
+
if (this._timestamps) {
|
|
217
|
+
this._timestamps.clear();
|
|
218
|
+
const now = Date.now();
|
|
219
|
+
for (const item of items) {
|
|
220
|
+
this._timestamps.set(item.id, now);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
let result = [...items];
|
|
224
|
+
if (this._maxSize > 0 && result.length > this._maxSize) {
|
|
225
|
+
result = this._evictForCapacity(result);
|
|
226
|
+
}
|
|
227
|
+
this._items = Object.freeze(result);
|
|
228
|
+
this.rebuildIndex();
|
|
229
|
+
this.notify(prev);
|
|
230
|
+
this._scheduleEvictionTimer();
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Remove all items.
|
|
234
|
+
*/
|
|
235
|
+
clear() {
|
|
236
|
+
if (this._disposed) {
|
|
237
|
+
throw new Error("Cannot clear disposed Collection");
|
|
238
|
+
}
|
|
239
|
+
if (this._items.length === 0) {
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const prev = this._items;
|
|
243
|
+
this._items = Object.freeze([]);
|
|
244
|
+
this._index.clear();
|
|
245
|
+
this._timestamps?.clear();
|
|
246
|
+
this._clearEvictionTimer();
|
|
247
|
+
this.notify(prev);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Snapshot current state, apply callback mutations, and return a rollback function.
|
|
251
|
+
* Rollback restores items to pre-callback state regardless of later mutations.
|
|
252
|
+
*/
|
|
253
|
+
optimistic(callback) {
|
|
254
|
+
if (this._disposed) {
|
|
255
|
+
throw new Error("Cannot perform optimistic update on disposed Collection");
|
|
256
|
+
}
|
|
257
|
+
const snapshot = this._items;
|
|
258
|
+
const timestampSnapshot = this._timestamps ? new Map(this._timestamps) : null;
|
|
259
|
+
callback();
|
|
260
|
+
let rolledBack = false;
|
|
261
|
+
return () => {
|
|
262
|
+
if (rolledBack || this._disposed) return;
|
|
263
|
+
rolledBack = true;
|
|
264
|
+
const prev = this._items;
|
|
265
|
+
this._items = snapshot;
|
|
266
|
+
if (timestampSnapshot) {
|
|
267
|
+
this._timestamps = timestampSnapshot;
|
|
268
|
+
}
|
|
269
|
+
this.rebuildIndex();
|
|
270
|
+
this.notify(prev);
|
|
271
|
+
this._scheduleEvictionTimer();
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
// ── Query Methods (pure, no notification) ──
|
|
275
|
+
/**
|
|
276
|
+
* Get item by id.
|
|
277
|
+
*/
|
|
278
|
+
get(id) {
|
|
279
|
+
return this._index.get(id);
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Check if item exists by id.
|
|
283
|
+
*/
|
|
284
|
+
has(id) {
|
|
285
|
+
return this._index.has(id);
|
|
286
|
+
}
|
|
287
|
+
/**
|
|
288
|
+
* Find first item matching predicate.
|
|
289
|
+
*/
|
|
290
|
+
find(predicate) {
|
|
291
|
+
return this._items.find(predicate);
|
|
292
|
+
}
|
|
293
|
+
/**
|
|
294
|
+
* Filter items matching predicate.
|
|
295
|
+
*/
|
|
296
|
+
filter(predicate) {
|
|
297
|
+
return this._items.filter(predicate);
|
|
298
|
+
}
|
|
299
|
+
/**
|
|
300
|
+
* Return sorted copy.
|
|
301
|
+
*/
|
|
302
|
+
sorted(compareFn) {
|
|
303
|
+
return [...this._items].sort(compareFn);
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Map items to new array.
|
|
307
|
+
*/
|
|
308
|
+
map(fn) {
|
|
309
|
+
return this._items.map(fn);
|
|
310
|
+
}
|
|
311
|
+
// ── Subscribable interface ──
|
|
312
|
+
/** Subscribes to state changes. Returns an unsubscribe function. */
|
|
313
|
+
subscribe(listener) {
|
|
314
|
+
if (this._disposed) {
|
|
315
|
+
return () => {
|
|
316
|
+
};
|
|
317
|
+
}
|
|
318
|
+
this._listeners.add(listener);
|
|
319
|
+
return () => {
|
|
320
|
+
this._listeners.delete(listener);
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
/** Tears down the instance, releasing all subscriptions and resources. */
|
|
324
|
+
dispose() {
|
|
325
|
+
if (this._disposed) {
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
this._disposed = true;
|
|
329
|
+
this._clearEvictionTimer();
|
|
330
|
+
this._abortController?.abort();
|
|
331
|
+
if (this._cleanups) {
|
|
332
|
+
for (const fn of this._cleanups) fn();
|
|
333
|
+
this._cleanups = null;
|
|
334
|
+
}
|
|
335
|
+
this.onDispose?.();
|
|
336
|
+
this._listeners.clear();
|
|
337
|
+
this._index.clear();
|
|
338
|
+
this._timestamps?.clear();
|
|
339
|
+
}
|
|
340
|
+
/** Registers a cleanup function to be called on dispose. @protected */
|
|
341
|
+
addCleanup(fn) {
|
|
342
|
+
if (!this._cleanups) {
|
|
343
|
+
this._cleanups = [];
|
|
344
|
+
}
|
|
345
|
+
this._cleanups.push(fn);
|
|
346
|
+
}
|
|
347
|
+
notify(prev) {
|
|
348
|
+
for (const listener of this._listeners) {
|
|
349
|
+
listener(this._items, prev);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
rebuildIndex() {
|
|
353
|
+
this._index.clear();
|
|
354
|
+
for (const item of this._items) {
|
|
355
|
+
this._index.set(item.id, item);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
// ── Eviction Internals ──
|
|
359
|
+
_evictForCapacity(items) {
|
|
360
|
+
const excess = items.length - this._maxSize;
|
|
361
|
+
if (excess <= 0) return items;
|
|
362
|
+
const candidates = items.slice(0, excess);
|
|
363
|
+
const toEvict = this._applyOnEvict(candidates, "capacity");
|
|
364
|
+
if (toEvict === false) return items;
|
|
365
|
+
if (toEvict.length === 0) return items;
|
|
366
|
+
const evictIds = new Set(toEvict.map((item) => item.id));
|
|
367
|
+
const result = items.filter((item) => !evictIds.has(item.id));
|
|
368
|
+
for (const item of toEvict) {
|
|
369
|
+
this._index.delete(item.id);
|
|
370
|
+
this._timestamps?.delete(item.id);
|
|
371
|
+
}
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
_applyOnEvict(candidates, reason) {
|
|
375
|
+
if (!this.onEvict) return candidates;
|
|
376
|
+
const result = this.onEvict(candidates, reason);
|
|
377
|
+
if (result === false) {
|
|
378
|
+
if (__DEV__ && reason === "capacity" && this._maxSize > 0) {
|
|
379
|
+
const currentSize = this._items.length + candidates.length;
|
|
380
|
+
if (currentSize > this._maxSize * 2) {
|
|
381
|
+
console.warn(
|
|
382
|
+
`[mvc-kit] Collection exceeded 2x MAX_SIZE (${currentSize}/${this._maxSize}). onEvict is vetoing eviction — this may cause unbounded growth.`
|
|
383
|
+
);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
if (Array.isArray(result)) {
|
|
389
|
+
const candidateIds = new Set(candidates.map((c) => c.id));
|
|
390
|
+
return result.filter((item) => candidateIds.has(item.id));
|
|
391
|
+
}
|
|
392
|
+
return candidates;
|
|
393
|
+
}
|
|
394
|
+
_sweepExpired() {
|
|
395
|
+
if (this._disposed || !this._timestamps || this._ttl <= 0) return;
|
|
396
|
+
const now = Date.now();
|
|
397
|
+
const ttl = this._ttl;
|
|
398
|
+
const expired = [];
|
|
399
|
+
for (const item of this._items) {
|
|
400
|
+
const ts = this._timestamps.get(item.id);
|
|
401
|
+
if (ts !== void 0 && now - ts >= ttl) {
|
|
402
|
+
expired.push(item);
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
if (expired.length === 0) {
|
|
406
|
+
this._scheduleEvictionTimer();
|
|
407
|
+
return;
|
|
408
|
+
}
|
|
409
|
+
const toEvict = this._applyOnEvict(expired, "ttl");
|
|
410
|
+
if (toEvict === false) {
|
|
411
|
+
this._scheduleEvictionTimer();
|
|
412
|
+
return;
|
|
413
|
+
}
|
|
414
|
+
if (toEvict.length === 0) {
|
|
415
|
+
this._scheduleEvictionTimer();
|
|
416
|
+
return;
|
|
417
|
+
}
|
|
418
|
+
const evictIds = new Set(toEvict.map((item) => item.id));
|
|
419
|
+
const prev = this._items;
|
|
420
|
+
this._items = Object.freeze(
|
|
421
|
+
prev.filter((item) => !evictIds.has(item.id))
|
|
422
|
+
);
|
|
423
|
+
for (const item of toEvict) {
|
|
424
|
+
this._index.delete(item.id);
|
|
425
|
+
this._timestamps.delete(item.id);
|
|
426
|
+
}
|
|
427
|
+
this.notify(prev);
|
|
428
|
+
this._scheduleEvictionTimer();
|
|
429
|
+
}
|
|
430
|
+
_scheduleEvictionTimer() {
|
|
431
|
+
this._clearEvictionTimer();
|
|
432
|
+
if (this._disposed || !this._timestamps || this._ttl <= 0 || this._timestamps.size === 0) return;
|
|
433
|
+
const now = Date.now();
|
|
434
|
+
const ttl = this._ttl;
|
|
435
|
+
let earliest = Infinity;
|
|
436
|
+
for (const ts of this._timestamps.values()) {
|
|
437
|
+
if (ts < earliest) earliest = ts;
|
|
438
|
+
}
|
|
439
|
+
const delay = Math.max(0, earliest + ttl - now);
|
|
440
|
+
this._evictionTimer = setTimeout(() => this._sweepExpired(), delay);
|
|
441
|
+
}
|
|
442
|
+
_clearEvictionTimer() {
|
|
443
|
+
if (this._evictionTimer !== null) {
|
|
444
|
+
clearTimeout(this._evictionTimer);
|
|
445
|
+
this._evictionTimer = null;
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
export {
|
|
450
|
+
Collection
|
|
451
|
+
};
|
|
452
|
+
//# sourceMappingURL=Collection.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Collection.js","sources":["../src/Collection.ts"],"sourcesContent":["import type { Listener, Subscribable } from './types';\n\nconst __DEV__ = typeof __MVC_KIT_DEV__ !== 'undefined' && __MVC_KIT_DEV__;\n\ntype CollectionState<T> = T[];\ntype CollectionListener<T> = Listener<CollectionState<T>>;\n\n/**\n * Reactive typed array with CRUD and query methods.\n */\nexport class Collection<T extends { id: string | number }> implements Subscribable<CollectionState<T>> {\n /** Maximum number of items before FIFO eviction. 0 = unlimited. */\n static MAX_SIZE = 0;\n /** Time-to-live in milliseconds. 0 = no expiry. */\n static TTL = 0;\n\n private _items: readonly T[] = [];\n private _disposed = false;\n private _listeners = new Set<CollectionListener<T>>();\n private _index = new Map<T['id'], T>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n private _timestamps: Map<T['id'], number> | null = null;\n private _evictionTimer: ReturnType<typeof setTimeout> | null = null;\n\n constructor(initialItems: T[] = []) {\n let result = [...initialItems];\n\n if (this._ttl > 0) {\n this._timestamps = new Map();\n const now = Date.now();\n for (const item of result) {\n this._timestamps.set(item.id, now);\n }\n }\n\n if (this._maxSize > 0 && result.length > this._maxSize) {\n // FIFO: trim from the front (oldest items)\n const excess = result.length - this._maxSize;\n const evicted = result.slice(0, excess);\n result = result.slice(excess);\n for (const item of evicted) {\n this._timestamps?.delete(item.id);\n }\n }\n\n this._items = Object.freeze(result);\n this.rebuildIndex();\n this._scheduleEvictionTimer();\n }\n\n /**\n * Alias for Subscribable compatibility.\n */\n get state(): T[] {\n return this._items as T[];\n }\n\n /** The raw array of items. */\n get items(): T[] {\n return this._items as T[];\n }\n\n /** Number of items in the collection. */\n get length(): number {\n return this._items.length;\n }\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n // ── Config Accessors ──\n\n private get _maxSize(): number {\n return (this.constructor as typeof Collection).MAX_SIZE;\n }\n\n private get _ttl(): number {\n return (this.constructor as typeof Collection).TTL;\n }\n\n // ── CRUD Methods (notify listeners) ──\n\n /**\n * Add one or more items. Items with existing IDs are silently skipped.\n */\n add(...items: T[]): void {\n if (this._disposed) {\n throw new Error('Cannot add to disposed Collection');\n }\n\n if (items.length === 0) {\n return;\n }\n\n const seen = new Set<T['id']>();\n const newItems: T[] = [];\n for (const item of items) {\n if (!this._index.has(item.id) && !seen.has(item.id)) {\n newItems.push(item);\n seen.add(item.id);\n }\n }\n if (newItems.length === 0) return;\n\n const prev = this._items;\n let result = [...prev, ...newItems];\n\n for (const item of newItems) {\n this._index.set(item.id, item);\n }\n\n // Record timestamps for TTL\n if (this._timestamps) {\n const now = Date.now();\n for (const item of newItems) {\n this._timestamps.set(item.id, now);\n }\n }\n\n // Enforce capacity before freeze/notify\n if (this._maxSize > 0 && result.length > this._maxSize) {\n result = this._evictForCapacity(result);\n }\n\n this._items = Object.freeze(result);\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n /**\n * Add or replace items by ID. Existing items are replaced in-place\n * (preserving array position); new items are appended. Deduplicates\n * input — last occurrence wins. No-op if nothing changed (reference\n * comparison).\n */\n upsert(...items: T[]): void {\n if (this._disposed) {\n throw new Error('Cannot upsert on disposed Collection');\n }\n if (items.length === 0) return;\n\n // Deduplicate input — last occurrence wins\n const incoming = new Map<T['id'], T>();\n for (const item of items) {\n incoming.set(item.id, item);\n }\n\n const prev = this._items;\n let changed = false;\n const replaced = new Set<T['id']>();\n const newArray: T[] = [];\n\n // Replace existing items in-place\n for (const existing of prev) {\n if (incoming.has(existing.id)) {\n const replacement = incoming.get(existing.id)!;\n if (replacement !== existing) changed = true;\n newArray.push(replacement);\n replaced.add(existing.id);\n } else {\n newArray.push(existing);\n }\n }\n\n // Append genuinely new items\n for (const [id, item] of incoming) {\n if (!replaced.has(id)) {\n newArray.push(item);\n changed = true;\n }\n }\n\n if (!changed) return;\n\n // Record/refresh timestamps for TTL (upsert refreshes existing)\n if (this._timestamps) {\n const now = Date.now();\n for (const [id] of incoming) {\n this._timestamps.set(id, now);\n }\n }\n\n for (const [id, item] of incoming) {\n this._index.set(id, item);\n }\n\n // Enforce capacity before freeze/notify\n let result = newArray;\n if (this._maxSize > 0 && result.length > this._maxSize) {\n result = this._evictForCapacity(result);\n }\n\n this._items = Object.freeze(result);\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n /**\n * Remove items by id(s).\n */\n remove(...ids: T['id'][]): void {\n if (this._disposed) {\n throw new Error('Cannot remove from disposed Collection');\n }\n\n if (ids.length === 0) {\n return;\n }\n\n const idSet = new Set(ids);\n const filtered = this._items.filter(item => !idSet.has(item.id));\n\n if (filtered.length === this._items.length) {\n return; // No items removed\n }\n\n const prev = this._items;\n this._items = Object.freeze(filtered);\n\n for (const id of ids) {\n this._index.delete(id);\n this._timestamps?.delete(id);\n }\n\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n /**\n * Update an item by id with partial changes.\n */\n update(id: T['id'], changes: Partial<T>): void {\n if (this._disposed) {\n throw new Error('Cannot update disposed Collection');\n }\n\n const idx = this._items.findIndex(item => item.id === id);\n if (idx === -1) {\n return;\n }\n\n const existing = this._items[idx];\n const updated = { ...existing, ...changes, id }; // Ensure id is preserved\n\n // Check if anything actually changed\n const keys = Object.keys(changes) as (keyof T)[];\n const hasChanges = keys.some(key => changes[key] !== existing[key]);\n if (!hasChanges) {\n return;\n }\n\n const prev = this._items;\n const newItems = [...prev];\n newItems[idx] = updated;\n this._items = Object.freeze(newItems);\n this._index.set(id, updated);\n\n this.notify(prev);\n }\n\n /**\n * Replace all items.\n */\n reset(items: T[]): void {\n if (this._disposed) {\n throw new Error('Cannot reset disposed Collection');\n }\n\n const prev = this._items;\n\n // Record timestamps for TTL\n if (this._timestamps) {\n this._timestamps.clear();\n const now = Date.now();\n for (const item of items) {\n this._timestamps.set(item.id, now);\n }\n }\n\n let result = [...items];\n\n // Enforce capacity before freeze/notify\n if (this._maxSize > 0 && result.length > this._maxSize) {\n result = this._evictForCapacity(result);\n }\n\n this._items = Object.freeze(result);\n this.rebuildIndex();\n\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n /**\n * Remove all items.\n */\n clear(): void {\n if (this._disposed) {\n throw new Error('Cannot clear disposed Collection');\n }\n\n if (this._items.length === 0) {\n return;\n }\n\n const prev = this._items;\n this._items = Object.freeze([]);\n this._index.clear();\n this._timestamps?.clear();\n this._clearEvictionTimer();\n\n this.notify(prev);\n }\n\n /**\n * Snapshot current state, apply callback mutations, and return a rollback function.\n * Rollback restores items to pre-callback state regardless of later mutations.\n */\n optimistic(callback: () => void): () => void {\n if (this._disposed) {\n throw new Error('Cannot perform optimistic update on disposed Collection');\n }\n\n const snapshot = this._items;\n const timestampSnapshot = this._timestamps ? new Map(this._timestamps) : null;\n callback();\n\n let rolledBack = false;\n return () => {\n if (rolledBack || this._disposed) return;\n rolledBack = true;\n\n const prev = this._items;\n this._items = snapshot;\n if (timestampSnapshot) {\n this._timestamps = timestampSnapshot;\n }\n this.rebuildIndex();\n this.notify(prev);\n this._scheduleEvictionTimer();\n };\n }\n\n // ── Query Methods (pure, no notification) ──\n\n /**\n * Get item by id.\n */\n get(id: T['id']): T | undefined {\n return this._index.get(id);\n }\n\n /**\n * Check if item exists by id.\n */\n has(id: T['id']): boolean {\n return this._index.has(id);\n }\n\n /**\n * Find first item matching predicate.\n */\n find(predicate: (item: T) => boolean): T | undefined {\n return this._items.find(predicate);\n }\n\n /**\n * Filter items matching predicate.\n */\n filter(predicate: (item: T) => boolean): T[] {\n return this._items.filter(predicate) as T[];\n }\n\n /**\n * Return sorted copy.\n */\n sorted(compareFn: (a: T, b: T) => number): T[] {\n return [...this._items].sort(compareFn);\n }\n\n /**\n * Map items to new array.\n */\n map<U>(fn: (item: T) => U): U[] {\n return this._items.map(fn);\n }\n\n // ── Subscribable interface ──\n\n /** Subscribes to state changes. Returns an unsubscribe function. */\n subscribe(listener: CollectionListener<T>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n this._listeners.add(listener);\n\n return () => {\n this._listeners.delete(listener);\n };\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n this._clearEvictionTimer();\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n this._listeners.clear();\n this._index.clear();\n this._timestamps?.clear();\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n\n /**\n * Called before items are auto-evicted by capacity or TTL.\n * Override to filter which items get evicted, or veto entirely.\n *\n * @param items - Candidates for eviction\n * @param reason - Why eviction is happening\n * @returns void to proceed with all, false to veto, or T[] subset to evict only those\n */\n protected onEvict?(items: T[], reason: 'capacity' | 'ttl'): T[] | false | void;\n\n private notify(prev: readonly T[]): void {\n for (const listener of this._listeners) {\n listener(this._items as T[], prev as T[]);\n }\n }\n\n private rebuildIndex(): void {\n this._index.clear();\n for (const item of this._items) {\n this._index.set(item.id, item);\n }\n }\n\n // ── Eviction Internals ──\n\n private _evictForCapacity(items: T[]): T[] {\n const excess = items.length - this._maxSize;\n if (excess <= 0) return items;\n\n const candidates = items.slice(0, excess);\n const toEvict = this._applyOnEvict(candidates, 'capacity');\n\n if (toEvict === false) return items; // veto\n\n if (toEvict.length === 0) return items; // nothing to evict\n\n const evictIds = new Set(toEvict.map(item => item.id));\n const result = items.filter(item => !evictIds.has(item.id));\n\n // Clean up index and timestamps for evicted items\n for (const item of toEvict) {\n this._index.delete(item.id);\n this._timestamps?.delete(item.id);\n }\n\n return result;\n }\n\n private _applyOnEvict(candidates: T[], reason: 'capacity' | 'ttl'): T[] | false {\n if (!this.onEvict) return candidates;\n\n const result = this.onEvict(candidates, reason);\n if (result === false) {\n // DEV warning when veto causes collection to exceed 2x MAX_SIZE\n if (__DEV__ && reason === 'capacity' && this._maxSize > 0) {\n const currentSize = this._items.length + candidates.length;\n if (currentSize > this._maxSize * 2) {\n console.warn(\n `[mvc-kit] Collection exceeded 2x MAX_SIZE (${currentSize}/${this._maxSize}). ` +\n `onEvict is vetoing eviction — this may cause unbounded growth.`\n );\n }\n }\n return false;\n }\n if (Array.isArray(result)) {\n // Only include items that are actually in the current items\n const candidateIds = new Set(candidates.map(c => c.id));\n return result.filter(item => candidateIds.has(item.id));\n }\n return candidates; // void = proceed with all\n }\n\n private _sweepExpired(): void {\n if (this._disposed || !this._timestamps || this._ttl <= 0) return;\n\n const now = Date.now();\n const ttl = this._ttl;\n const expired: T[] = [];\n\n for (const item of this._items) {\n const ts = this._timestamps.get(item.id);\n if (ts !== undefined && (now - ts) >= ttl) {\n expired.push(item);\n }\n }\n\n if (expired.length === 0) {\n this._scheduleEvictionTimer();\n return;\n }\n\n const toEvict = this._applyOnEvict(expired, 'ttl');\n\n if (toEvict === false) {\n this._scheduleEvictionTimer();\n return;\n }\n\n if (toEvict.length === 0) {\n this._scheduleEvictionTimer();\n return;\n }\n\n const evictIds = new Set(toEvict.map(item => item.id));\n const prev = this._items;\n this._items = Object.freeze(\n (prev as T[]).filter((item: T) => !evictIds.has(item.id))\n );\n\n for (const item of toEvict) {\n this._index.delete(item.id);\n this._timestamps.delete(item.id);\n }\n\n this.notify(prev);\n this._scheduleEvictionTimer();\n }\n\n private _scheduleEvictionTimer(): void {\n this._clearEvictionTimer();\n\n if (this._disposed || !this._timestamps || this._ttl <= 0 || this._timestamps.size === 0) return;\n\n const now = Date.now();\n const ttl = this._ttl;\n let earliest = Infinity;\n\n for (const ts of this._timestamps.values()) {\n if (ts < earliest) earliest = ts;\n }\n\n const delay = Math.max(0, (earliest + ttl) - now);\n this._evictionTimer = setTimeout(() => this._sweepExpired(), delay);\n }\n\n private _clearEvictionTimer(): void {\n if (this._evictionTimer !== null) {\n clearTimeout(this._evictionTimer);\n this._evictionTimer = null;\n }\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
class Controller {
|
|
4
|
+
_disposed = false;
|
|
5
|
+
_initialized = false;
|
|
6
|
+
_abortController = null;
|
|
7
|
+
_cleanups = null;
|
|
8
|
+
/** Whether this instance has been disposed. */
|
|
9
|
+
get disposed() {
|
|
10
|
+
return this._disposed;
|
|
11
|
+
}
|
|
12
|
+
/** Whether init() has been called. */
|
|
13
|
+
get initialized() {
|
|
14
|
+
return this._initialized;
|
|
15
|
+
}
|
|
16
|
+
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
|
17
|
+
get disposeSignal() {
|
|
18
|
+
if (!this._abortController) {
|
|
19
|
+
this._abortController = new AbortController();
|
|
20
|
+
}
|
|
21
|
+
return this._abortController.signal;
|
|
22
|
+
}
|
|
23
|
+
/** Initializes the instance. Called automatically by React hooks after mount. */
|
|
24
|
+
init() {
|
|
25
|
+
if (this._initialized || this._disposed) return;
|
|
26
|
+
this._initialized = true;
|
|
27
|
+
return this.onInit?.();
|
|
28
|
+
}
|
|
29
|
+
/** Tears down the instance, releasing all subscriptions and resources. */
|
|
30
|
+
dispose() {
|
|
31
|
+
if (this._disposed) {
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
this._disposed = true;
|
|
35
|
+
this._abortController?.abort();
|
|
36
|
+
if (this._cleanups) {
|
|
37
|
+
for (const fn of this._cleanups) fn();
|
|
38
|
+
this._cleanups = null;
|
|
39
|
+
}
|
|
40
|
+
this.onDispose?.();
|
|
41
|
+
}
|
|
42
|
+
/** Registers a cleanup function to be called on dispose. @protected */
|
|
43
|
+
addCleanup(fn) {
|
|
44
|
+
if (!this._cleanups) {
|
|
45
|
+
this._cleanups = [];
|
|
46
|
+
}
|
|
47
|
+
this._cleanups.push(fn);
|
|
48
|
+
}
|
|
49
|
+
/** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
|
|
50
|
+
subscribeTo(source, listener) {
|
|
51
|
+
const unsubscribe = source.subscribe(listener);
|
|
52
|
+
this.addCleanup(unsubscribe);
|
|
53
|
+
return unsubscribe;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
exports.Controller = Controller;
|
|
57
|
+
//# sourceMappingURL=Controller.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Controller.cjs","sources":["../src/Controller.ts"],"sourcesContent":["import type { Disposable, Subscribable, Listener } from './types';\n\n/**\n * Base class for stateless orchestrators.\n * Controllers coordinate between ViewModels, Models, and Services.\n */\nexport abstract class Controller implements Disposable {\n private _disposed = false;\n private _initialized = false;\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this._disposed) return;\n this._initialized = true;\n return this.onInit?.();\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */\n protected subscribeTo<T>(source: Subscribable<T>, listener: Listener<T>): () => void {\n const unsubscribe = source.subscribe(listener);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n}\n"],"names":[],"mappings":";;AAMO,MAAe,WAAiC;AAAA,EAC7C,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,mBAA2C;AAAA,EAC3C,YAAmC;AAAA;AAAA,EAG3C,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,cAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,gBAA6B;AAC/B,QAAI,CAAC,KAAK,kBAAkB;AAC1B,WAAK,mBAAmB,IAAI,gBAAA;AAAA,IAC9B;AACA,WAAO,KAAK,iBAAiB;AAAA,EAC/B;AAAA;AAAA,EAGA,OAA6B;AAC3B,QAAI,KAAK,gBAAgB,KAAK,UAAW;AACzC,SAAK,eAAe;AACpB,WAAO,KAAK,SAAA;AAAA,EACd;AAAA;AAAA,EAGA,UAAgB;AACd,QAAI,KAAK,WAAW;AAClB;AAAA,IACF;AAEA,SAAK,YAAY;AACjB,SAAK,kBAAkB,MAAA;AACvB,QAAI,KAAK,WAAW;AAClB,iBAAW,MAAM,KAAK,UAAW,IAAA;AACjC,WAAK,YAAY;AAAA,IACnB;AACA,SAAK,YAAA;AAAA,EACP;AAAA;AAAA,EAGU,WAAW,IAAsB;AACzC,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,YAAY,CAAA;AAAA,IACnB;AACA,SAAK,UAAU,KAAK,EAAE;AAAA,EACxB;AAAA;AAAA,EAGU,YAAe,QAAyB,UAAmC;AACnF,UAAM,cAAc,OAAO,UAAU,QAAQ;AAC7C,SAAK,WAAW,WAAW;AAC3B,WAAO;AAAA,EACT;AAMF;;"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
class Controller {
|
|
2
|
+
_disposed = false;
|
|
3
|
+
_initialized = false;
|
|
4
|
+
_abortController = null;
|
|
5
|
+
_cleanups = null;
|
|
6
|
+
/** Whether this instance has been disposed. */
|
|
7
|
+
get disposed() {
|
|
8
|
+
return this._disposed;
|
|
9
|
+
}
|
|
10
|
+
/** Whether init() has been called. */
|
|
11
|
+
get initialized() {
|
|
12
|
+
return this._initialized;
|
|
13
|
+
}
|
|
14
|
+
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
|
15
|
+
get disposeSignal() {
|
|
16
|
+
if (!this._abortController) {
|
|
17
|
+
this._abortController = new AbortController();
|
|
18
|
+
}
|
|
19
|
+
return this._abortController.signal;
|
|
20
|
+
}
|
|
21
|
+
/** Initializes the instance. Called automatically by React hooks after mount. */
|
|
22
|
+
init() {
|
|
23
|
+
if (this._initialized || this._disposed) return;
|
|
24
|
+
this._initialized = true;
|
|
25
|
+
return this.onInit?.();
|
|
26
|
+
}
|
|
27
|
+
/** Tears down the instance, releasing all subscriptions and resources. */
|
|
28
|
+
dispose() {
|
|
29
|
+
if (this._disposed) {
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this._disposed = true;
|
|
33
|
+
this._abortController?.abort();
|
|
34
|
+
if (this._cleanups) {
|
|
35
|
+
for (const fn of this._cleanups) fn();
|
|
36
|
+
this._cleanups = null;
|
|
37
|
+
}
|
|
38
|
+
this.onDispose?.();
|
|
39
|
+
}
|
|
40
|
+
/** Registers a cleanup function to be called on dispose. @protected */
|
|
41
|
+
addCleanup(fn) {
|
|
42
|
+
if (!this._cleanups) {
|
|
43
|
+
this._cleanups = [];
|
|
44
|
+
}
|
|
45
|
+
this._cleanups.push(fn);
|
|
46
|
+
}
|
|
47
|
+
/** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
|
|
48
|
+
subscribeTo(source, listener) {
|
|
49
|
+
const unsubscribe = source.subscribe(listener);
|
|
50
|
+
this.addCleanup(unsubscribe);
|
|
51
|
+
return unsubscribe;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
export {
|
|
55
|
+
Controller
|
|
56
|
+
};
|
|
57
|
+
//# sourceMappingURL=Controller.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Controller.js","sources":["../src/Controller.ts"],"sourcesContent":["import type { Disposable, Subscribable, Listener } from './types';\n\n/**\n * Base class for stateless orchestrators.\n * Controllers coordinate between ViewModels, Models, and Services.\n */\nexport abstract class Controller implements Disposable {\n private _disposed = false;\n private _initialized = false;\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n /** Whether this instance has been disposed. */\n get disposed(): boolean {\n return this._disposed;\n }\n\n /** Whether init() has been called. */\n get initialized(): boolean {\n return this._initialized;\n }\n\n /** AbortSignal that fires when this instance is disposed. Lazily created. */\n get disposeSignal(): AbortSignal {\n if (!this._abortController) {\n this._abortController = new AbortController();\n }\n return this._abortController.signal;\n }\n\n /** Initializes the instance. Called automatically by React hooks after mount. */\n init(): void | Promise<void> {\n if (this._initialized || this._disposed) return;\n this._initialized = true;\n return this.onInit?.();\n }\n\n /** Tears down the instance, releasing all subscriptions and resources. */\n dispose(): void {\n if (this._disposed) {\n return;\n }\n\n this._disposed = true;\n this._abortController?.abort();\n if (this._cleanups) {\n for (const fn of this._cleanups) fn();\n this._cleanups = null;\n }\n this.onDispose?.();\n }\n\n /** Registers a cleanup function to be called on dispose. @protected */\n protected addCleanup(fn: () => void): void {\n if (!this._cleanups) {\n this._cleanups = [];\n }\n this._cleanups.push(fn);\n }\n\n /** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */\n protected subscribeTo<T>(source: Subscribable<T>, listener: Listener<T>): () => void {\n const unsubscribe = source.subscribe(listener);\n this.addCleanup(unsubscribe);\n return unsubscribe;\n }\n\n /** Lifecycle hook called at the end of init(). Override to load initial data. @protected */\n protected onInit?(): void | Promise<void>;\n /** Lifecycle hook called during dispose(). Override for custom teardown. @protected */\n protected onDispose?(): void;\n}\n"],"names":[],"mappings":"AAMO,MAAe,WAAiC;AAAA,EAC7C,YAAY;AAAA,EACZ,eAAe;AAAA,EACf,mBAA2C;AAAA,EAC3C,YAAmC;AAAA;AAAA,EAG3C,IAAI,WAAoB;AACtB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,cAAuB;AACzB,WAAO,KAAK;AAAA,EACd;AAAA;AAAA,EAGA,IAAI,gBAA6B;AAC/B,QAAI,CAAC,KAAK,kBAAkB;AAC1B,WAAK,mBAAmB,IAAI,gBAAA;AAAA,IAC9B;AACA,WAAO,KAAK,iBAAiB;AAAA,EAC/B;AAAA;AAAA,EAGA,OAA6B;AAC3B,QAAI,KAAK,gBAAgB,KAAK,UAAW;AACzC,SAAK,eAAe;AACpB,WAAO,KAAK,SAAA;AAAA,EACd;AAAA;AAAA,EAGA,UAAgB;AACd,QAAI,KAAK,WAAW;AAClB;AAAA,IACF;AAEA,SAAK,YAAY;AACjB,SAAK,kBAAkB,MAAA;AACvB,QAAI,KAAK,WAAW;AAClB,iBAAW,MAAM,KAAK,UAAW,IAAA;AACjC,WAAK,YAAY;AAAA,IACnB;AACA,SAAK,YAAA;AAAA,EACP;AAAA;AAAA,EAGU,WAAW,IAAsB;AACzC,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,YAAY,CAAA;AAAA,IACnB;AACA,SAAK,UAAU,KAAK,EAAE;AAAA,EACxB;AAAA;AAAA,EAGU,YAAe,QAAyB,UAAmC;AACnF,UAAM,cAAc,OAAO,UAAU,QAAQ;AAC7C,SAAK,WAAW,WAAW;AAC3B,WAAO;AAAA,EACT;AAMF;"}
|