mvc-kit 2.5.3 → 2.5.5
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 +583 -0
- package/dist/ViewModel.cjs.map +1 -0
- package/dist/ViewModel.d.ts +6 -9
- package/dist/ViewModel.d.ts.map +1 -1
- package/dist/ViewModel.js +583 -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,84 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
class EventBus {
|
|
4
|
+
_disposed = false;
|
|
5
|
+
_handlers = /* @__PURE__ */ new Map();
|
|
6
|
+
_abortController = null;
|
|
7
|
+
_cleanups = null;
|
|
8
|
+
/** Whether this instance has been disposed. */
|
|
9
|
+
get disposed() {
|
|
10
|
+
return this._disposed;
|
|
11
|
+
}
|
|
12
|
+
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
|
13
|
+
get disposeSignal() {
|
|
14
|
+
if (!this._abortController) {
|
|
15
|
+
this._abortController = new AbortController();
|
|
16
|
+
}
|
|
17
|
+
return this._abortController.signal;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Emit an event with a payload.
|
|
21
|
+
*/
|
|
22
|
+
emit(event, payload) {
|
|
23
|
+
if (this._disposed) {
|
|
24
|
+
throw new Error("Cannot emit on disposed EventBus");
|
|
25
|
+
}
|
|
26
|
+
const handlers = this._handlers.get(event);
|
|
27
|
+
if (handlers) {
|
|
28
|
+
for (const handler of handlers) {
|
|
29
|
+
handler(payload);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Subscribe to an event. Returns unsubscribe function.
|
|
35
|
+
*/
|
|
36
|
+
on(event, handler) {
|
|
37
|
+
if (this._disposed) {
|
|
38
|
+
return () => {
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
let handlers = this._handlers.get(event);
|
|
42
|
+
if (!handlers) {
|
|
43
|
+
handlers = /* @__PURE__ */ new Set();
|
|
44
|
+
this._handlers.set(event, handlers);
|
|
45
|
+
}
|
|
46
|
+
handlers.add(handler);
|
|
47
|
+
return () => {
|
|
48
|
+
handlers.delete(handler);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Subscribe to an event once. Auto-unsubscribes after first invocation.
|
|
53
|
+
*/
|
|
54
|
+
once(event, handler) {
|
|
55
|
+
const unsubscribe = this.on(event, (payload) => {
|
|
56
|
+
unsubscribe();
|
|
57
|
+
handler(payload);
|
|
58
|
+
});
|
|
59
|
+
return unsubscribe;
|
|
60
|
+
}
|
|
61
|
+
/** Tears down the instance, releasing all subscriptions and resources. */
|
|
62
|
+
dispose() {
|
|
63
|
+
if (this._disposed) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this._disposed = true;
|
|
67
|
+
this._abortController?.abort();
|
|
68
|
+
if (this._cleanups) {
|
|
69
|
+
for (const fn of this._cleanups) fn();
|
|
70
|
+
this._cleanups = null;
|
|
71
|
+
}
|
|
72
|
+
this.onDispose?.();
|
|
73
|
+
this._handlers.clear();
|
|
74
|
+
}
|
|
75
|
+
/** Registers a cleanup function to be called on dispose. @protected */
|
|
76
|
+
addCleanup(fn) {
|
|
77
|
+
if (!this._cleanups) {
|
|
78
|
+
this._cleanups = [];
|
|
79
|
+
}
|
|
80
|
+
this._cleanups.push(fn);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
exports.EventBus = EventBus;
|
|
84
|
+
//# sourceMappingURL=EventBus.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventBus.cjs","sources":["../src/EventBus.ts"],"sourcesContent":["import type { Disposable } from './types';\n\ntype Handler<T> = (payload: T) => void;\n\n/**\n * Typed pub/sub event bus.\n */\nexport class EventBus<E extends Record<string, any>> implements Disposable {\n /** Phantom type brand — enables correct inference of E in generic helpers like useEvent(). */\n declare readonly _types: E;\n\n private _disposed = false;\n private _handlers = new Map<keyof E, Set<Handler<unknown>>>();\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 /** 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 /**\n * Emit an event with a payload.\n */\n emit<K extends keyof E>(event: K, payload: E[K]): void {\n if (this._disposed) {\n throw new Error('Cannot emit on disposed EventBus');\n }\n\n const handlers = this._handlers.get(event);\n if (handlers) {\n for (const handler of handlers) {\n handler(payload);\n }\n }\n }\n\n /**\n * Subscribe to an event. Returns unsubscribe function.\n */\n on<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n let handlers = this._handlers.get(event);\n if (!handlers) {\n handlers = new Set();\n this._handlers.set(event, handlers);\n }\n\n handlers.add(handler as Handler<unknown>);\n\n return () => {\n handlers!.delete(handler as Handler<unknown>);\n };\n }\n\n /**\n * Subscribe to an event once. Auto-unsubscribes after first invocation.\n */\n once<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n const unsubscribe = this.on(event, (payload) => {\n unsubscribe();\n handler(payload);\n });\n return unsubscribe;\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 this._handlers.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"],"names":[],"mappings":";;AAOO,MAAM,SAA8D;AAAA,EAIjE,YAAY;AAAA,EACZ,gCAAgB,IAAA;AAAA,EAChB,mBAA2C;AAAA,EAC3C,YAAmC;AAAA;AAAA,EAG3C,IAAI,WAAoB;AACtB,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;AAAA;AAAA,EAKA,KAAwB,OAAU,SAAqB;AACrD,QAAI,KAAK,WAAW;AAClB,YAAM,IAAI,MAAM,kCAAkC;AAAA,IACpD;AAEA,UAAM,WAAW,KAAK,UAAU,IAAI,KAAK;AACzC,QAAI,UAAU;AACZ,iBAAW,WAAW,UAAU;AAC9B,gBAAQ,OAAO;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,GAAsB,OAAU,SAAoC;AAClE,QAAI,KAAK,WAAW;AAClB,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AAEA,QAAI,WAAW,KAAK,UAAU,IAAI,KAAK;AACvC,QAAI,CAAC,UAAU;AACb,qCAAe,IAAA;AACf,WAAK,UAAU,IAAI,OAAO,QAAQ;AAAA,IACpC;AAEA,aAAS,IAAI,OAA2B;AAExC,WAAO,MAAM;AACX,eAAU,OAAO,OAA2B;AAAA,IAC9C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,KAAwB,OAAU,SAAoC;AACpE,UAAM,cAAc,KAAK,GAAG,OAAO,CAAC,YAAY;AAC9C,kBAAA;AACA,cAAQ,OAAO;AAAA,IACjB,CAAC;AACD,WAAO;AAAA,EACT;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;AACL,SAAK,UAAU,MAAA;AAAA,EACjB;AAAA;AAAA,EAGU,WAAW,IAAsB;AACzC,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,YAAY,CAAA;AAAA,IACnB;AACA,SAAK,UAAU,KAAK,EAAE;AAAA,EACxB;AAIF;;"}
|
package/dist/EventBus.js
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
class EventBus {
|
|
2
|
+
_disposed = false;
|
|
3
|
+
_handlers = /* @__PURE__ */ new Map();
|
|
4
|
+
_abortController = null;
|
|
5
|
+
_cleanups = null;
|
|
6
|
+
/** Whether this instance has been disposed. */
|
|
7
|
+
get disposed() {
|
|
8
|
+
return this._disposed;
|
|
9
|
+
}
|
|
10
|
+
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
|
11
|
+
get disposeSignal() {
|
|
12
|
+
if (!this._abortController) {
|
|
13
|
+
this._abortController = new AbortController();
|
|
14
|
+
}
|
|
15
|
+
return this._abortController.signal;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Emit an event with a payload.
|
|
19
|
+
*/
|
|
20
|
+
emit(event, payload) {
|
|
21
|
+
if (this._disposed) {
|
|
22
|
+
throw new Error("Cannot emit on disposed EventBus");
|
|
23
|
+
}
|
|
24
|
+
const handlers = this._handlers.get(event);
|
|
25
|
+
if (handlers) {
|
|
26
|
+
for (const handler of handlers) {
|
|
27
|
+
handler(payload);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* Subscribe to an event. Returns unsubscribe function.
|
|
33
|
+
*/
|
|
34
|
+
on(event, handler) {
|
|
35
|
+
if (this._disposed) {
|
|
36
|
+
return () => {
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
let handlers = this._handlers.get(event);
|
|
40
|
+
if (!handlers) {
|
|
41
|
+
handlers = /* @__PURE__ */ new Set();
|
|
42
|
+
this._handlers.set(event, handlers);
|
|
43
|
+
}
|
|
44
|
+
handlers.add(handler);
|
|
45
|
+
return () => {
|
|
46
|
+
handlers.delete(handler);
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Subscribe to an event once. Auto-unsubscribes after first invocation.
|
|
51
|
+
*/
|
|
52
|
+
once(event, handler) {
|
|
53
|
+
const unsubscribe = this.on(event, (payload) => {
|
|
54
|
+
unsubscribe();
|
|
55
|
+
handler(payload);
|
|
56
|
+
});
|
|
57
|
+
return unsubscribe;
|
|
58
|
+
}
|
|
59
|
+
/** Tears down the instance, releasing all subscriptions and resources. */
|
|
60
|
+
dispose() {
|
|
61
|
+
if (this._disposed) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
this._disposed = true;
|
|
65
|
+
this._abortController?.abort();
|
|
66
|
+
if (this._cleanups) {
|
|
67
|
+
for (const fn of this._cleanups) fn();
|
|
68
|
+
this._cleanups = null;
|
|
69
|
+
}
|
|
70
|
+
this.onDispose?.();
|
|
71
|
+
this._handlers.clear();
|
|
72
|
+
}
|
|
73
|
+
/** Registers a cleanup function to be called on dispose. @protected */
|
|
74
|
+
addCleanup(fn) {
|
|
75
|
+
if (!this._cleanups) {
|
|
76
|
+
this._cleanups = [];
|
|
77
|
+
}
|
|
78
|
+
this._cleanups.push(fn);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
EventBus
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=EventBus.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"EventBus.js","sources":["../src/EventBus.ts"],"sourcesContent":["import type { Disposable } from './types';\n\ntype Handler<T> = (payload: T) => void;\n\n/**\n * Typed pub/sub event bus.\n */\nexport class EventBus<E extends Record<string, any>> implements Disposable {\n /** Phantom type brand — enables correct inference of E in generic helpers like useEvent(). */\n declare readonly _types: E;\n\n private _disposed = false;\n private _handlers = new Map<keyof E, Set<Handler<unknown>>>();\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 /** 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 /**\n * Emit an event with a payload.\n */\n emit<K extends keyof E>(event: K, payload: E[K]): void {\n if (this._disposed) {\n throw new Error('Cannot emit on disposed EventBus');\n }\n\n const handlers = this._handlers.get(event);\n if (handlers) {\n for (const handler of handlers) {\n handler(payload);\n }\n }\n }\n\n /**\n * Subscribe to an event. Returns unsubscribe function.\n */\n on<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n if (this._disposed) {\n return () => {};\n }\n\n let handlers = this._handlers.get(event);\n if (!handlers) {\n handlers = new Set();\n this._handlers.set(event, handlers);\n }\n\n handlers.add(handler as Handler<unknown>);\n\n return () => {\n handlers!.delete(handler as Handler<unknown>);\n };\n }\n\n /**\n * Subscribe to an event once. Auto-unsubscribes after first invocation.\n */\n once<K extends keyof E>(event: K, handler: Handler<E[K]>): () => void {\n const unsubscribe = this.on(event, (payload) => {\n unsubscribe();\n handler(payload);\n });\n return unsubscribe;\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 this._handlers.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"],"names":[],"mappings":"AAOO,MAAM,SAA8D;AAAA,EAIjE,YAAY;AAAA,EACZ,gCAAgB,IAAA;AAAA,EAChB,mBAA2C;AAAA,EAC3C,YAAmC;AAAA;AAAA,EAG3C,IAAI,WAAoB;AACtB,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;AAAA;AAAA,EAKA,KAAwB,OAAU,SAAqB;AACrD,QAAI,KAAK,WAAW;AAClB,YAAM,IAAI,MAAM,kCAAkC;AAAA,IACpD;AAEA,UAAM,WAAW,KAAK,UAAU,IAAI,KAAK;AACzC,QAAI,UAAU;AACZ,iBAAW,WAAW,UAAU;AAC9B,gBAAQ,OAAO;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,GAAsB,OAAU,SAAoC;AAClE,QAAI,KAAK,WAAW;AAClB,aAAO,MAAM;AAAA,MAAC;AAAA,IAChB;AAEA,QAAI,WAAW,KAAK,UAAU,IAAI,KAAK;AACvC,QAAI,CAAC,UAAU;AACb,qCAAe,IAAA;AACf,WAAK,UAAU,IAAI,OAAO,QAAQ;AAAA,IACpC;AAEA,aAAS,IAAI,OAA2B;AAExC,WAAO,MAAM;AACX,eAAU,OAAO,OAA2B;AAAA,IAC9C;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,KAAwB,OAAU,SAAoC;AACpE,UAAM,cAAc,KAAK,GAAG,OAAO,CAAC,YAAY;AAC9C,kBAAA;AACA,cAAQ,OAAO;AAAA,IACjB,CAAC;AACD,WAAO;AAAA,EACT;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;AACL,SAAK,UAAU,MAAA;AAAA,EACjB;AAAA;AAAA,EAGU,WAAW,IAAsB;AACzC,QAAI,CAAC,KAAK,WAAW;AACnB,WAAK,YAAY,CAAA;AAAA,IACnB;AACA,SAAK,UAAU,KAAK,EAAE;AAAA,EACxB;AAIF;"}
|
package/dist/Model.cjs
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
|
+
class Model {
|
|
4
|
+
_state;
|
|
5
|
+
_committed;
|
|
6
|
+
_disposed = false;
|
|
7
|
+
_initialized = false;
|
|
8
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
9
|
+
_abortController = null;
|
|
10
|
+
_cleanups = null;
|
|
11
|
+
constructor(initialState) {
|
|
12
|
+
const frozen = Object.freeze({ ...initialState });
|
|
13
|
+
this._state = frozen;
|
|
14
|
+
this._committed = frozen;
|
|
15
|
+
}
|
|
16
|
+
/** Current frozen state object. */
|
|
17
|
+
get state() {
|
|
18
|
+
return this._state;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* The baseline state for dirty tracking.
|
|
22
|
+
*/
|
|
23
|
+
get committed() {
|
|
24
|
+
return this._committed;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* True if current state differs from committed state.
|
|
28
|
+
*/
|
|
29
|
+
get dirty() {
|
|
30
|
+
return !this.shallowEqual(this._state, this._committed);
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Validation errors for the current state.
|
|
34
|
+
*/
|
|
35
|
+
get errors() {
|
|
36
|
+
return this.validate(this._state);
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* True if there are no validation errors.
|
|
40
|
+
*/
|
|
41
|
+
get valid() {
|
|
42
|
+
return Object.keys(this.errors).length === 0;
|
|
43
|
+
}
|
|
44
|
+
/** Whether this instance has been disposed. */
|
|
45
|
+
get disposed() {
|
|
46
|
+
return this._disposed;
|
|
47
|
+
}
|
|
48
|
+
/** Whether init() has been called. */
|
|
49
|
+
get initialized() {
|
|
50
|
+
return this._initialized;
|
|
51
|
+
}
|
|
52
|
+
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
|
53
|
+
get disposeSignal() {
|
|
54
|
+
if (!this._abortController) {
|
|
55
|
+
this._abortController = new AbortController();
|
|
56
|
+
}
|
|
57
|
+
return this._abortController.signal;
|
|
58
|
+
}
|
|
59
|
+
/** Initializes the instance. Called automatically by React hooks after mount. */
|
|
60
|
+
init() {
|
|
61
|
+
if (this._initialized || this._disposed) return;
|
|
62
|
+
this._initialized = true;
|
|
63
|
+
return this.onInit?.();
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Merges partial state with validation. No-op if no values changed by reference.
|
|
67
|
+
* @protected
|
|
68
|
+
*/
|
|
69
|
+
set(partialOrUpdater) {
|
|
70
|
+
if (this._disposed) {
|
|
71
|
+
throw new Error("Cannot set state on disposed Model");
|
|
72
|
+
}
|
|
73
|
+
const partial = typeof partialOrUpdater === "function" ? partialOrUpdater(this._state) : partialOrUpdater;
|
|
74
|
+
const keys = Object.keys(partial);
|
|
75
|
+
const hasChanges = keys.some(
|
|
76
|
+
(key) => partial[key] !== this._state[key]
|
|
77
|
+
);
|
|
78
|
+
if (!hasChanges) {
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
const prev = this._state;
|
|
82
|
+
const next = Object.freeze({ ...prev, ...partial });
|
|
83
|
+
this._state = next;
|
|
84
|
+
this.onSet?.(prev, next);
|
|
85
|
+
for (const listener of this._listeners) {
|
|
86
|
+
listener(next, prev);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Mark current state as the new baseline (not dirty).
|
|
91
|
+
*/
|
|
92
|
+
commit() {
|
|
93
|
+
if (this._disposed) {
|
|
94
|
+
throw new Error("Cannot commit on disposed Model");
|
|
95
|
+
}
|
|
96
|
+
this._committed = this._state;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Revert state to committed baseline.
|
|
100
|
+
*/
|
|
101
|
+
rollback() {
|
|
102
|
+
if (this._disposed) {
|
|
103
|
+
throw new Error("Cannot rollback on disposed Model");
|
|
104
|
+
}
|
|
105
|
+
if (this.shallowEqual(this._state, this._committed)) {
|
|
106
|
+
return;
|
|
107
|
+
}
|
|
108
|
+
const prev = this._state;
|
|
109
|
+
this._state = this._committed;
|
|
110
|
+
this.onSet?.(prev, this._state);
|
|
111
|
+
for (const listener of this._listeners) {
|
|
112
|
+
listener(this._state, prev);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
/** Subscribes to state changes. Returns an unsubscribe function. */
|
|
116
|
+
subscribe(listener) {
|
|
117
|
+
if (this._disposed) {
|
|
118
|
+
return () => {
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
this._listeners.add(listener);
|
|
122
|
+
return () => {
|
|
123
|
+
this._listeners.delete(listener);
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
/** Tears down the instance, releasing all subscriptions and resources. */
|
|
127
|
+
dispose() {
|
|
128
|
+
if (this._disposed) {
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
this._disposed = true;
|
|
132
|
+
this._abortController?.abort();
|
|
133
|
+
if (this._cleanups) {
|
|
134
|
+
for (const fn of this._cleanups) fn();
|
|
135
|
+
this._cleanups = null;
|
|
136
|
+
}
|
|
137
|
+
this.onDispose?.();
|
|
138
|
+
this._listeners.clear();
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Override to provide validation logic.
|
|
142
|
+
* Return an object mapping field keys to error messages.
|
|
143
|
+
*/
|
|
144
|
+
validate(_state) {
|
|
145
|
+
return {};
|
|
146
|
+
}
|
|
147
|
+
/** Registers a cleanup function to be called on dispose. @protected */
|
|
148
|
+
addCleanup(fn) {
|
|
149
|
+
if (!this._cleanups) {
|
|
150
|
+
this._cleanups = [];
|
|
151
|
+
}
|
|
152
|
+
this._cleanups.push(fn);
|
|
153
|
+
}
|
|
154
|
+
/** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
|
|
155
|
+
subscribeTo(source, listener) {
|
|
156
|
+
const unsubscribe = source.subscribe(listener);
|
|
157
|
+
this.addCleanup(unsubscribe);
|
|
158
|
+
return unsubscribe;
|
|
159
|
+
}
|
|
160
|
+
shallowEqual(a, b) {
|
|
161
|
+
const keysA = Object.keys(a);
|
|
162
|
+
const keysB = Object.keys(b);
|
|
163
|
+
if (keysA.length !== keysB.length) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
for (const key of keysA) {
|
|
167
|
+
if (a[key] !== b[key]) {
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
return true;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
exports.Model = Model;
|
|
175
|
+
//# sourceMappingURL=Model.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Model.cjs","sources":["../src/Model.ts"],"sourcesContent":["import type { Listener, Updater, Subscribable, ValidationErrors } from './types';\n\n/**\n * Reactive entity with validation and dirty tracking.\n */\nexport abstract class Model<S extends object> implements Subscribable<S> {\n private _state: Readonly<S>;\n private _committed: Readonly<S>;\n private _disposed = false;\n private _initialized = false;\n private _listeners = new Set<Listener<S>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n constructor(initialState: S) {\n const frozen = Object.freeze({ ...initialState });\n this._state = frozen;\n this._committed = frozen;\n }\n\n /** Current frozen state object. */\n get state(): S {\n return this._state;\n }\n\n /**\n * The baseline state for dirty tracking.\n */\n get committed(): S {\n return this._committed;\n }\n\n /**\n * True if current state differs from committed state.\n */\n get dirty(): boolean {\n return !this.shallowEqual(this._state, this._committed);\n }\n\n /**\n * Validation errors for the current state.\n */\n get errors(): ValidationErrors<S> {\n return this.validate(this._state);\n }\n\n /**\n * True if there are no validation errors.\n */\n get valid(): boolean {\n return Object.keys(this.errors).length === 0;\n }\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 /**\n * Merges partial state with validation. No-op if no values changed by reference.\n * @protected\n */\n protected set(partialOrUpdater: Partial<S> | Updater<S>): void {\n if (this._disposed) {\n throw new Error('Cannot set state on disposed Model');\n }\n\n const partial =\n typeof partialOrUpdater === 'function'\n ? partialOrUpdater(this._state)\n : partialOrUpdater;\n\n // Check if any values actually changed (shallow equality)\n const keys = Object.keys(partial) as (keyof S)[];\n const hasChanges = keys.some(\n (key) => partial[key] !== this._state[key]\n );\n\n if (!hasChanges) {\n return;\n }\n\n const prev = this._state;\n const next = Object.freeze({ ...prev, ...partial });\n this._state = next;\n\n this.onSet?.(prev, next);\n\n for (const listener of this._listeners) {\n listener(next, prev);\n }\n }\n\n /**\n * Mark current state as the new baseline (not dirty).\n */\n commit(): void {\n if (this._disposed) {\n throw new Error('Cannot commit on disposed Model');\n }\n this._committed = this._state;\n }\n\n /**\n * Revert state to committed baseline.\n */\n rollback(): void {\n if (this._disposed) {\n throw new Error('Cannot rollback on disposed Model');\n }\n\n if (this.shallowEqual(this._state, this._committed)) {\n return;\n }\n\n const prev = this._state;\n this._state = this._committed;\n\n this.onSet?.(prev, this._state);\n\n for (const listener of this._listeners) {\n listener(this._state, prev);\n }\n }\n\n /** Subscribes to state changes. Returns an unsubscribe function. */\n subscribe(listener: Listener<S>): () => 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._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 }\n\n /**\n * Override to provide validation logic.\n * Return an object mapping field keys to error messages.\n */\n protected validate(_state: S): ValidationErrors<S> {\n return {};\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 after every set() with the previous state. @protected */\n protected onSet?(prev: S, next: S): void;\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 private shallowEqual(a: S, b: S): boolean {\n const keysA = Object.keys(a) as (keyof S)[];\n const keysB = Object.keys(b) as (keyof S)[];\n\n if (keysA.length !== keysB.length) {\n return false;\n }\n\n for (const key of keysA) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/Model.js
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
class Model {
|
|
2
|
+
_state;
|
|
3
|
+
_committed;
|
|
4
|
+
_disposed = false;
|
|
5
|
+
_initialized = false;
|
|
6
|
+
_listeners = /* @__PURE__ */ new Set();
|
|
7
|
+
_abortController = null;
|
|
8
|
+
_cleanups = null;
|
|
9
|
+
constructor(initialState) {
|
|
10
|
+
const frozen = Object.freeze({ ...initialState });
|
|
11
|
+
this._state = frozen;
|
|
12
|
+
this._committed = frozen;
|
|
13
|
+
}
|
|
14
|
+
/** Current frozen state object. */
|
|
15
|
+
get state() {
|
|
16
|
+
return this._state;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* The baseline state for dirty tracking.
|
|
20
|
+
*/
|
|
21
|
+
get committed() {
|
|
22
|
+
return this._committed;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* True if current state differs from committed state.
|
|
26
|
+
*/
|
|
27
|
+
get dirty() {
|
|
28
|
+
return !this.shallowEqual(this._state, this._committed);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Validation errors for the current state.
|
|
32
|
+
*/
|
|
33
|
+
get errors() {
|
|
34
|
+
return this.validate(this._state);
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* True if there are no validation errors.
|
|
38
|
+
*/
|
|
39
|
+
get valid() {
|
|
40
|
+
return Object.keys(this.errors).length === 0;
|
|
41
|
+
}
|
|
42
|
+
/** Whether this instance has been disposed. */
|
|
43
|
+
get disposed() {
|
|
44
|
+
return this._disposed;
|
|
45
|
+
}
|
|
46
|
+
/** Whether init() has been called. */
|
|
47
|
+
get initialized() {
|
|
48
|
+
return this._initialized;
|
|
49
|
+
}
|
|
50
|
+
/** AbortSignal that fires when this instance is disposed. Lazily created. */
|
|
51
|
+
get disposeSignal() {
|
|
52
|
+
if (!this._abortController) {
|
|
53
|
+
this._abortController = new AbortController();
|
|
54
|
+
}
|
|
55
|
+
return this._abortController.signal;
|
|
56
|
+
}
|
|
57
|
+
/** Initializes the instance. Called automatically by React hooks after mount. */
|
|
58
|
+
init() {
|
|
59
|
+
if (this._initialized || this._disposed) return;
|
|
60
|
+
this._initialized = true;
|
|
61
|
+
return this.onInit?.();
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Merges partial state with validation. No-op if no values changed by reference.
|
|
65
|
+
* @protected
|
|
66
|
+
*/
|
|
67
|
+
set(partialOrUpdater) {
|
|
68
|
+
if (this._disposed) {
|
|
69
|
+
throw new Error("Cannot set state on disposed Model");
|
|
70
|
+
}
|
|
71
|
+
const partial = typeof partialOrUpdater === "function" ? partialOrUpdater(this._state) : partialOrUpdater;
|
|
72
|
+
const keys = Object.keys(partial);
|
|
73
|
+
const hasChanges = keys.some(
|
|
74
|
+
(key) => partial[key] !== this._state[key]
|
|
75
|
+
);
|
|
76
|
+
if (!hasChanges) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
const prev = this._state;
|
|
80
|
+
const next = Object.freeze({ ...prev, ...partial });
|
|
81
|
+
this._state = next;
|
|
82
|
+
this.onSet?.(prev, next);
|
|
83
|
+
for (const listener of this._listeners) {
|
|
84
|
+
listener(next, prev);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Mark current state as the new baseline (not dirty).
|
|
89
|
+
*/
|
|
90
|
+
commit() {
|
|
91
|
+
if (this._disposed) {
|
|
92
|
+
throw new Error("Cannot commit on disposed Model");
|
|
93
|
+
}
|
|
94
|
+
this._committed = this._state;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Revert state to committed baseline.
|
|
98
|
+
*/
|
|
99
|
+
rollback() {
|
|
100
|
+
if (this._disposed) {
|
|
101
|
+
throw new Error("Cannot rollback on disposed Model");
|
|
102
|
+
}
|
|
103
|
+
if (this.shallowEqual(this._state, this._committed)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
const prev = this._state;
|
|
107
|
+
this._state = this._committed;
|
|
108
|
+
this.onSet?.(prev, this._state);
|
|
109
|
+
for (const listener of this._listeners) {
|
|
110
|
+
listener(this._state, prev);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
/** Subscribes to state changes. Returns an unsubscribe function. */
|
|
114
|
+
subscribe(listener) {
|
|
115
|
+
if (this._disposed) {
|
|
116
|
+
return () => {
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
this._listeners.add(listener);
|
|
120
|
+
return () => {
|
|
121
|
+
this._listeners.delete(listener);
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** Tears down the instance, releasing all subscriptions and resources. */
|
|
125
|
+
dispose() {
|
|
126
|
+
if (this._disposed) {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
this._disposed = true;
|
|
130
|
+
this._abortController?.abort();
|
|
131
|
+
if (this._cleanups) {
|
|
132
|
+
for (const fn of this._cleanups) fn();
|
|
133
|
+
this._cleanups = null;
|
|
134
|
+
}
|
|
135
|
+
this.onDispose?.();
|
|
136
|
+
this._listeners.clear();
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* Override to provide validation logic.
|
|
140
|
+
* Return an object mapping field keys to error messages.
|
|
141
|
+
*/
|
|
142
|
+
validate(_state) {
|
|
143
|
+
return {};
|
|
144
|
+
}
|
|
145
|
+
/** Registers a cleanup function to be called on dispose. @protected */
|
|
146
|
+
addCleanup(fn) {
|
|
147
|
+
if (!this._cleanups) {
|
|
148
|
+
this._cleanups = [];
|
|
149
|
+
}
|
|
150
|
+
this._cleanups.push(fn);
|
|
151
|
+
}
|
|
152
|
+
/** Subscribes to an external Subscribable with automatic cleanup on dispose. @protected */
|
|
153
|
+
subscribeTo(source, listener) {
|
|
154
|
+
const unsubscribe = source.subscribe(listener);
|
|
155
|
+
this.addCleanup(unsubscribe);
|
|
156
|
+
return unsubscribe;
|
|
157
|
+
}
|
|
158
|
+
shallowEqual(a, b) {
|
|
159
|
+
const keysA = Object.keys(a);
|
|
160
|
+
const keysB = Object.keys(b);
|
|
161
|
+
if (keysA.length !== keysB.length) {
|
|
162
|
+
return false;
|
|
163
|
+
}
|
|
164
|
+
for (const key of keysA) {
|
|
165
|
+
if (a[key] !== b[key]) {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export {
|
|
173
|
+
Model
|
|
174
|
+
};
|
|
175
|
+
//# sourceMappingURL=Model.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Model.js","sources":["../src/Model.ts"],"sourcesContent":["import type { Listener, Updater, Subscribable, ValidationErrors } from './types';\n\n/**\n * Reactive entity with validation and dirty tracking.\n */\nexport abstract class Model<S extends object> implements Subscribable<S> {\n private _state: Readonly<S>;\n private _committed: Readonly<S>;\n private _disposed = false;\n private _initialized = false;\n private _listeners = new Set<Listener<S>>();\n private _abortController: AbortController | null = null;\n private _cleanups: (() => void)[] | null = null;\n\n constructor(initialState: S) {\n const frozen = Object.freeze({ ...initialState });\n this._state = frozen;\n this._committed = frozen;\n }\n\n /** Current frozen state object. */\n get state(): S {\n return this._state;\n }\n\n /**\n * The baseline state for dirty tracking.\n */\n get committed(): S {\n return this._committed;\n }\n\n /**\n * True if current state differs from committed state.\n */\n get dirty(): boolean {\n return !this.shallowEqual(this._state, this._committed);\n }\n\n /**\n * Validation errors for the current state.\n */\n get errors(): ValidationErrors<S> {\n return this.validate(this._state);\n }\n\n /**\n * True if there are no validation errors.\n */\n get valid(): boolean {\n return Object.keys(this.errors).length === 0;\n }\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 /**\n * Merges partial state with validation. No-op if no values changed by reference.\n * @protected\n */\n protected set(partialOrUpdater: Partial<S> | Updater<S>): void {\n if (this._disposed) {\n throw new Error('Cannot set state on disposed Model');\n }\n\n const partial =\n typeof partialOrUpdater === 'function'\n ? partialOrUpdater(this._state)\n : partialOrUpdater;\n\n // Check if any values actually changed (shallow equality)\n const keys = Object.keys(partial) as (keyof S)[];\n const hasChanges = keys.some(\n (key) => partial[key] !== this._state[key]\n );\n\n if (!hasChanges) {\n return;\n }\n\n const prev = this._state;\n const next = Object.freeze({ ...prev, ...partial });\n this._state = next;\n\n this.onSet?.(prev, next);\n\n for (const listener of this._listeners) {\n listener(next, prev);\n }\n }\n\n /**\n * Mark current state as the new baseline (not dirty).\n */\n commit(): void {\n if (this._disposed) {\n throw new Error('Cannot commit on disposed Model');\n }\n this._committed = this._state;\n }\n\n /**\n * Revert state to committed baseline.\n */\n rollback(): void {\n if (this._disposed) {\n throw new Error('Cannot rollback on disposed Model');\n }\n\n if (this.shallowEqual(this._state, this._committed)) {\n return;\n }\n\n const prev = this._state;\n this._state = this._committed;\n\n this.onSet?.(prev, this._state);\n\n for (const listener of this._listeners) {\n listener(this._state, prev);\n }\n }\n\n /** Subscribes to state changes. Returns an unsubscribe function. */\n subscribe(listener: Listener<S>): () => 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._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 }\n\n /**\n * Override to provide validation logic.\n * Return an object mapping field keys to error messages.\n */\n protected validate(_state: S): ValidationErrors<S> {\n return {};\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 after every set() with the previous state. @protected */\n protected onSet?(prev: S, next: S): void;\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 private shallowEqual(a: S, b: S): boolean {\n const keysA = Object.keys(a) as (keyof S)[];\n const keysB = Object.keys(b) as (keyof S)[];\n\n if (keysA.length !== keysB.length) {\n return false;\n }\n\n for (const key of keysA) {\n if (a[key] !== b[key]) {\n return false;\n }\n }\n\n return true;\n }\n}\n"],"names":[],"mappings":"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"}
|