@virentia/react 0.1.0

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 ADDED
@@ -0,0 +1,94 @@
1
+ # @virentia/react
2
+
3
+ React bindings for Virentia core models.
4
+
5
+ Keep business logic in `@virentia/core`; use this package at the rendering boundary. Stores become render values, events and effects become callbacks bound to the provided scope.
6
+
7
+ ## Links
8
+
9
+ - Documentation: [movpushmov.dev/virentia/react](https://movpushmov.dev/virentia/react/)
10
+
11
+ ## Install
12
+
13
+ ```sh
14
+ pnpm add @virentia/react
15
+ ```
16
+
17
+ ## ScopeProvider
18
+
19
+ ```tsx
20
+ import { scope } from "@virentia/core";
21
+ import { ScopeProvider } from "@virentia/react";
22
+
23
+ const appScope = scope();
24
+
25
+ export function App() {
26
+ return (
27
+ <ScopeProvider scope={appScope}>
28
+ <Routes />
29
+ </ScopeProvider>
30
+ );
31
+ }
32
+ ```
33
+
34
+ ## useUnit
35
+
36
+ ```tsx
37
+ import { useUnit } from "@virentia/react";
38
+ import { counterModel } from "./counter.model";
39
+
40
+ export function CounterButton() {
41
+ const count = useUnit(counterModel.count);
42
+ const incremented = useUnit(counterModel.incremented);
43
+
44
+ return <button onClick={() => incremented(1)}>{count}</button>;
45
+ }
46
+ ```
47
+
48
+ ## component
49
+
50
+ `component` pairs a model factory with a view. The model receives lifecycle units and props through `ModelContext`; the view receives an unwrapped model.
51
+
52
+ ```tsx
53
+ import { event, reaction, store } from "@virentia/core";
54
+ import { component, type ModelContext } from "@virentia/react";
55
+
56
+ function createCounterModel({ props }: ModelContext<{ step: number }>) {
57
+ const clicked = event<void>();
58
+ const count = store(0);
59
+
60
+ reaction({
61
+ on: clicked,
62
+ run() {
63
+ count.value += props.step;
64
+ },
65
+ });
66
+
67
+ return { clicked, count };
68
+ }
69
+
70
+ export const Counter = component({
71
+ model: createCounterModel,
72
+ view({ model }) {
73
+ return <button onClick={() => model.clicked()}>{model.count}</button>;
74
+ },
75
+ });
76
+ ```
77
+
78
+ ## Model Caches
79
+
80
+ Use `createModelCache` when a model should survive unmount and be reused by key: chats, tabs, detail screens, media players, or previews.
81
+
82
+ ```ts
83
+ import { createModelCache } from "@virentia/react";
84
+
85
+ const chatCache = createModelCache<string, ChatProps, ChatModel>();
86
+ ```
87
+
88
+ ## Main API
89
+
90
+ `ScopeProvider`, `useProvidedScope`, `useUnit`, `useModel`, `component`, `createModelCache`.
91
+
92
+ ## License
93
+
94
+ MIT © 2026 movpushmov
package/dist/index.cjs ADDED
@@ -0,0 +1,275 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let react = require("react");
3
+ let _virentia_core = require("@virentia/core");
4
+ //#region lib/model-cache.ts
5
+ function createModelCache() {
6
+ const byScope = /* @__PURE__ */ new WeakMap();
7
+ const maps = /* @__PURE__ */ new Set();
8
+ const getScopeMap = (scope) => {
9
+ let map = byScope.get(scope);
10
+ if (!map) {
11
+ map = /* @__PURE__ */ new Map();
12
+ byScope.set(scope, map);
13
+ maps.add(map);
14
+ }
15
+ return map;
16
+ };
17
+ const find = (key, scope) => {
18
+ if (scope) return byScope.get(scope)?.get(key);
19
+ for (const map of maps) {
20
+ const instance = map.get(key);
21
+ if (instance) return instance;
22
+ }
23
+ };
24
+ return {
25
+ has(key, scope) {
26
+ return Boolean(find(key, scope));
27
+ },
28
+ get(key, scope) {
29
+ return find(key, scope)?.model;
30
+ },
31
+ getInstance(key, scope) {
32
+ return find(key, scope);
33
+ },
34
+ delete(key, scope) {
35
+ if (scope) {
36
+ const map = byScope.get(scope);
37
+ const instance = map?.get(key);
38
+ if (!instance) return false;
39
+ instance.dispose();
40
+ map?.delete(key);
41
+ return true;
42
+ }
43
+ let deleted = false;
44
+ for (const map of maps) {
45
+ const instance = map.get(key);
46
+ if (!instance) continue;
47
+ instance.dispose();
48
+ map.delete(key);
49
+ deleted = true;
50
+ }
51
+ return deleted;
52
+ },
53
+ clear(scope) {
54
+ if (scope) {
55
+ const map = byScope.get(scope);
56
+ if (!map) return;
57
+ for (const instance of map.values()) instance.dispose();
58
+ map.clear();
59
+ return;
60
+ }
61
+ for (const map of maps) {
62
+ for (const instance of map.values()) instance.dispose();
63
+ map.clear();
64
+ }
65
+ },
66
+ [modelCacheInternal](scope, key, create) {
67
+ const map = getScopeMap(scope);
68
+ let instance = map.get(key);
69
+ if (!instance) {
70
+ instance = create();
71
+ map.set(key, instance);
72
+ }
73
+ return instance;
74
+ }
75
+ };
76
+ }
77
+ function getOrCreateCachedInstance(cache, scope, key, create) {
78
+ const internal = cache[modelCacheInternal];
79
+ if (!internal) throw new Error("[useModel] Unsupported model cache. Use createModelCache().");
80
+ return internal(scope, key, create);
81
+ }
82
+ const modelCacheInternal = Symbol("virentia.react.modelCacheInternal");
83
+ //#endregion
84
+ //#region lib/scope.ts
85
+ const ScopeContext = (0, react.createContext)(null);
86
+ function ScopeProvider(props) {
87
+ return (0, react.createElement)(ScopeContext.Provider, { value: props.scope }, props.children);
88
+ }
89
+ function useProvidedScope() {
90
+ const scope = (0, react.useContext)(ScopeContext);
91
+ if (!scope) throw new Error("[useProvidedScope] Scope is not provided. Wrap your tree with ScopeProvider.");
92
+ return scope;
93
+ }
94
+ //#endregion
95
+ //#region lib/utils.ts
96
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? react.useEffect : react.useLayoutEffect;
97
+ function readStore(unit, scope) {
98
+ return (0, _virentia_core.scoped)(scope, () => {
99
+ const keys = Reflect.ownKeys(unit).filter((key) => !nativeStoreKeys.has(key));
100
+ if (keys.length === 1 && keys[0] === "value") return Reflect.get(unit, "value");
101
+ if (isArraySnapshot(unit, keys)) {
102
+ const length = Reflect.get(unit, "length");
103
+ return Array.from({ length }, (_value, index) => Reflect.get(unit, String(index)));
104
+ }
105
+ return Object.fromEntries(keys.map((key) => [key, Reflect.get(unit, key)]));
106
+ });
107
+ }
108
+ function writeStore(unit, value, scope) {
109
+ (0, _virentia_core.run)({
110
+ unit: unit.node,
111
+ payload: value,
112
+ scope
113
+ });
114
+ }
115
+ function isUnitLike(value) {
116
+ return isStoreUnit(value) || isCallableUnit(value);
117
+ }
118
+ function isStoreUnit(value) {
119
+ return Boolean(value && (typeof value === "object" || typeof value === "function") && "node" in value && "subscribe" in value && typeof value.subscribe === "function");
120
+ }
121
+ function isCallableUnit(value) {
122
+ return Boolean(typeof value === "function" && "node" in value);
123
+ }
124
+ function isPlainObject(value) {
125
+ if (!value || typeof value !== "object") return false;
126
+ const prototype = Object.getPrototypeOf(value);
127
+ return prototype === Object.prototype || prototype === null;
128
+ }
129
+ function getComponentName(view) {
130
+ return `Virentia(${view.displayName ?? view.name ?? "Component"})`;
131
+ }
132
+ function isArraySnapshot(unit, keys) {
133
+ return keys.includes("length") && keys.every((key) => key === "length" || typeof key === "string" && arrayIndexPattern.test(key)) && typeof Reflect.get(unit, "length") === "number";
134
+ }
135
+ const arrayIndexPattern = /^(0|[1-9]\d*)$/;
136
+ const nativeStoreKeys = new Set([
137
+ "node",
138
+ "writable",
139
+ "subscribe",
140
+ "map",
141
+ "filter",
142
+ "filterMap"
143
+ ]);
144
+ //#endregion
145
+ //#region lib/use-unit.ts
146
+ function useUnit(input) {
147
+ return useUnitWithScope(input, useProvidedScope());
148
+ }
149
+ function useUnitWithScope(input, scope) {
150
+ if (Array.isArray(input)) return input.map((unit) => useSingleUnit(unit, scope));
151
+ if (isUnitLike(input)) return useSingleUnit(input, scope);
152
+ const result = {};
153
+ for (const key of Object.keys(input)) result[key] = useSingleUnit(input[key], scope);
154
+ return result;
155
+ }
156
+ function useSingleUnit(unit, scope) {
157
+ if (isStoreUnit(unit)) return useStoreUnit(unit, scope);
158
+ return (0, react.useCallback)((...args) => (0, _virentia_core.scoped)(scope, () => unit(...args)), [scope, unit]);
159
+ }
160
+ function useStoreUnit(unit, scope) {
161
+ const snapshotRef = (0, react.useRef)(readStore(unit, scope));
162
+ snapshotRef.current = readStore(unit, scope);
163
+ const subscribe = (0, react.useCallback)((notify) => {
164
+ const unsubscribe = unit.subscribe((_value, nextScope) => {
165
+ if (nextScope !== scope) return;
166
+ snapshotRef.current = readStore(unit, scope);
167
+ notify();
168
+ });
169
+ snapshotRef.current = readStore(unit, scope);
170
+ return unsubscribe;
171
+ }, [scope, unit]);
172
+ const getSnapshot = (0, react.useCallback)(() => snapshotRef.current, []);
173
+ return (0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot);
174
+ }
175
+ //#endregion
176
+ //#region lib/use-model.ts
177
+ function useModel(modelOrFactory, props, options) {
178
+ const scope = useProvidedScope();
179
+ if (typeof modelOrFactory !== "function") return useReactiveModel(modelOrFactory, scope);
180
+ return useReactiveModel(useModelInstance(modelOrFactory, props, scope, options).model, scope);
181
+ }
182
+ function useModelInstance(factory, props, scope, options) {
183
+ const cache = options?.cache;
184
+ const key = options?.key;
185
+ const cached = Boolean(cache);
186
+ const instance = (0, react.useMemo)(() => {
187
+ if (cache) return getOrCreateCachedInstance(cache, scope, key, () => createModelInstance(factory, props, scope, key));
188
+ return createModelInstance(factory, props, scope, void 0);
189
+ }, [
190
+ cache,
191
+ key,
192
+ scope
193
+ ]);
194
+ useIsomorphicLayoutEffect(() => {
195
+ writeStore(instance.props, props, scope);
196
+ }, [
197
+ instance,
198
+ props,
199
+ scope
200
+ ]);
201
+ (0, react.useEffect)(() => {
202
+ (0, _virentia_core.scoped)(scope, () => {
203
+ instance.mounts.value += 1;
204
+ instance.mounted();
205
+ });
206
+ return () => {
207
+ (0, _virentia_core.scoped)(scope, () => {
208
+ instance.mounts.value = Math.max(0, instance.mounts.value - 1);
209
+ instance.unmounted();
210
+ });
211
+ if (!cached) instance.dispose();
212
+ };
213
+ }, [
214
+ cached,
215
+ instance,
216
+ scope
217
+ ]);
218
+ return instance;
219
+ }
220
+ function createModelInstance(factory, props, scope, key) {
221
+ return (0, _virentia_core.owner)((dispose, modelOwner) => {
222
+ const context = {
223
+ scope,
224
+ owner: modelOwner,
225
+ props: (0, _virentia_core.store)(props),
226
+ mounted: (0, _virentia_core.event)(),
227
+ unmounted: (0, _virentia_core.event)(),
228
+ mounts: (0, _virentia_core.store)(0),
229
+ key
230
+ };
231
+ const model = (0, _virentia_core.scoped)(scope, () => factory(context));
232
+ return {
233
+ ...context,
234
+ model,
235
+ dispose
236
+ };
237
+ });
238
+ }
239
+ function useReactiveModel(model, scope) {
240
+ const result = {};
241
+ for (const key of Reflect.ownKeys(model)) {
242
+ if (key === "dispose" || key === disposeSymbol) continue;
243
+ result[key] = useModelValue(Reflect.get(model, key), scope);
244
+ }
245
+ return result;
246
+ }
247
+ const disposeSymbol = typeof Symbol.dispose === "symbol" ? Symbol.dispose : Symbol.for("Symbol.dispose");
248
+ function useModelValue(value, scope) {
249
+ if (isUnitLike(value)) return useUnitWithScope(value, scope);
250
+ if (isPlainObject(value)) return useReactiveModel(value, scope);
251
+ return value;
252
+ }
253
+ //#endregion
254
+ //#region lib/component.ts
255
+ function component(config) {
256
+ const VirentiaComponent = (props) => {
257
+ const model = "cache" in config ? useModel(config.model, props, {
258
+ cache: config.cache,
259
+ key: config.key(props)
260
+ }) : useModel(config.model, props);
261
+ return (0, react.createElement)(config.view, {
262
+ ...props,
263
+ model
264
+ });
265
+ };
266
+ VirentiaComponent.displayName = getComponentName(config.view);
267
+ return VirentiaComponent;
268
+ }
269
+ //#endregion
270
+ exports.ScopeProvider = ScopeProvider;
271
+ exports.component = component;
272
+ exports.createModelCache = createModelCache;
273
+ exports.useModel = useModel;
274
+ exports.useProvidedScope = useProvidedScope;
275
+ exports.useUnit = useUnit;
@@ -0,0 +1,73 @@
1
+ import { ComponentType, FC, ReactNode } from "react";
2
+ import { Effect, EffectCallArgs, EventCallable, EventPayload, Owner, Scope, Store, StoreWritable } from "@virentia/core";
3
+
4
+ //#region lib/types.d.ts
5
+ type UnitLike = Store<any> | StoreWritable<any> | EventCallable<any> | Effect<any, any, any>;
6
+ type UnitValue<Unit> = Unit extends Store<infer State> | StoreWritable<infer State> ? State : Unit extends EventCallable<infer Payload> ? (...payload: EventPayload<Payload>) => Promise<void> : Unit extends Effect<infer Params, infer Done, any> ? (...args: EffectCallArgs<Params>) => Promise<Done> : never;
7
+ type ReactiveModel<Model> = { readonly [Key in keyof Model as Key extends "dispose" ? never : Key]: Model[Key] extends UnitLike ? UnitValue<Model[Key]> : Model[Key] extends ((...args: any[]) => any) ? Model[Key] : Model[Key] extends object ? ReactiveModel<Model[Key]> : Model[Key] };
8
+ interface ModelContext<Props, Key = undefined> {
9
+ readonly scope: Scope;
10
+ readonly owner: Owner;
11
+ readonly props: StoreWritable<Props>;
12
+ readonly mounted: EventCallable<void>;
13
+ readonly unmounted: EventCallable<void>;
14
+ readonly mounts: StoreWritable<number>;
15
+ readonly key: Key;
16
+ }
17
+ type ModelFactory<Props, Model extends object, Key = undefined> = (context: ModelContext<Props, Key>) => Model;
18
+ interface ModelInstance<Props, Model extends object, Key = undefined> extends ModelContext<Props, Key> {
19
+ readonly model: Model;
20
+ dispose(): void;
21
+ }
22
+ interface ModelCache<Key, Props, Model extends object> {
23
+ has(key: Key, scope?: Scope): boolean;
24
+ get(key: Key, scope?: Scope): Model | undefined;
25
+ getInstance(key: Key, scope?: Scope): ModelInstance<Props, Model, Key> | undefined;
26
+ delete(key: Key, scope?: Scope): boolean;
27
+ clear(scope?: Scope): void;
28
+ }
29
+ type UnitShape<Shape> = Shape extends readonly UnitLike[] ? { readonly [Key in keyof Shape]: UnitValue<Shape[Key]> } : Shape extends Record<string, UnitLike> ? { readonly [Key in keyof Shape]: UnitValue<Shape[Key]> } : never;
30
+ type CacheOptions<Props, Key, Model extends object> = {
31
+ readonly cache: ModelCache<Key, Props, Model>;
32
+ readonly key: Key;
33
+ };
34
+ type ComponentConfig<Props, Model extends object> = {
35
+ readonly model: ModelFactory<Props, Model>;
36
+ readonly view: ComponentType<Props & {
37
+ model: ReactiveModel<Model>;
38
+ }>;
39
+ };
40
+ type CachedComponentConfig<Props, Key, Model extends object> = {
41
+ readonly key: (props: Props) => Key;
42
+ readonly cache: ModelCache<Key, Props, Model>;
43
+ readonly model: ModelFactory<Props, Model, Key>;
44
+ readonly view: ComponentType<Props & {
45
+ model: ReactiveModel<Model>;
46
+ }>;
47
+ };
48
+ //#endregion
49
+ //#region lib/component.d.ts
50
+ declare function component<Props, Model extends object>(config: ComponentConfig<Props, Model>): FC<Props>;
51
+ declare function component<Props, Key, Model extends object>(config: CachedComponentConfig<Props, Key, Model>): FC<Props>;
52
+ //#endregion
53
+ //#region lib/model-cache.d.ts
54
+ declare function createModelCache<Key, Props, Model extends object>(): ModelCache<Key, Props, Model>;
55
+ //#endregion
56
+ //#region lib/scope.d.ts
57
+ declare function ScopeProvider(props: {
58
+ scope: Scope;
59
+ children?: ReactNode;
60
+ }): ReactNode;
61
+ declare function useProvidedScope(): Scope;
62
+ //#endregion
63
+ //#region lib/use-model.d.ts
64
+ declare function useModel<Model extends object>(model: Model): ReactiveModel<Model>;
65
+ declare function useModel<Props, Model extends object>(factory: ModelFactory<Props, Model>, props: Props): ReactiveModel<Model>;
66
+ declare function useModel<Props, Key, Model extends object>(factory: ModelFactory<Props, Model, Key>, props: Props, options: CacheOptions<Props, Key, Model>): ReactiveModel<Model>;
67
+ //#endregion
68
+ //#region lib/use-unit.d.ts
69
+ declare function useUnit<Unit extends UnitLike>(unit: Unit): UnitValue<Unit>;
70
+ declare function useUnit<Shape extends readonly UnitLike[]>(shape: Shape): UnitShape<Shape>;
71
+ declare function useUnit<Shape extends Record<string, UnitLike>>(shape: Shape): UnitShape<Shape>;
72
+ //#endregion
73
+ export { type ModelCache, type ModelContext, type ModelFactory, type ModelInstance, type ReactiveModel, ScopeProvider, type UnitLike, type UnitValue, component, createModelCache, useModel, useProvidedScope, useUnit };
@@ -0,0 +1,73 @@
1
+ import { ComponentType, FC, ReactNode } from "react";
2
+ import { Effect, EffectCallArgs, EventCallable, EventPayload, Owner, Scope, Store, StoreWritable } from "@virentia/core";
3
+
4
+ //#region lib/types.d.ts
5
+ type UnitLike = Store<any> | StoreWritable<any> | EventCallable<any> | Effect<any, any, any>;
6
+ type UnitValue<Unit> = Unit extends Store<infer State> | StoreWritable<infer State> ? State : Unit extends EventCallable<infer Payload> ? (...payload: EventPayload<Payload>) => Promise<void> : Unit extends Effect<infer Params, infer Done, any> ? (...args: EffectCallArgs<Params>) => Promise<Done> : never;
7
+ type ReactiveModel<Model> = { readonly [Key in keyof Model as Key extends "dispose" ? never : Key]: Model[Key] extends UnitLike ? UnitValue<Model[Key]> : Model[Key] extends ((...args: any[]) => any) ? Model[Key] : Model[Key] extends object ? ReactiveModel<Model[Key]> : Model[Key] };
8
+ interface ModelContext<Props, Key = undefined> {
9
+ readonly scope: Scope;
10
+ readonly owner: Owner;
11
+ readonly props: StoreWritable<Props>;
12
+ readonly mounted: EventCallable<void>;
13
+ readonly unmounted: EventCallable<void>;
14
+ readonly mounts: StoreWritable<number>;
15
+ readonly key: Key;
16
+ }
17
+ type ModelFactory<Props, Model extends object, Key = undefined> = (context: ModelContext<Props, Key>) => Model;
18
+ interface ModelInstance<Props, Model extends object, Key = undefined> extends ModelContext<Props, Key> {
19
+ readonly model: Model;
20
+ dispose(): void;
21
+ }
22
+ interface ModelCache<Key, Props, Model extends object> {
23
+ has(key: Key, scope?: Scope): boolean;
24
+ get(key: Key, scope?: Scope): Model | undefined;
25
+ getInstance(key: Key, scope?: Scope): ModelInstance<Props, Model, Key> | undefined;
26
+ delete(key: Key, scope?: Scope): boolean;
27
+ clear(scope?: Scope): void;
28
+ }
29
+ type UnitShape<Shape> = Shape extends readonly UnitLike[] ? { readonly [Key in keyof Shape]: UnitValue<Shape[Key]> } : Shape extends Record<string, UnitLike> ? { readonly [Key in keyof Shape]: UnitValue<Shape[Key]> } : never;
30
+ type CacheOptions<Props, Key, Model extends object> = {
31
+ readonly cache: ModelCache<Key, Props, Model>;
32
+ readonly key: Key;
33
+ };
34
+ type ComponentConfig<Props, Model extends object> = {
35
+ readonly model: ModelFactory<Props, Model>;
36
+ readonly view: ComponentType<Props & {
37
+ model: ReactiveModel<Model>;
38
+ }>;
39
+ };
40
+ type CachedComponentConfig<Props, Key, Model extends object> = {
41
+ readonly key: (props: Props) => Key;
42
+ readonly cache: ModelCache<Key, Props, Model>;
43
+ readonly model: ModelFactory<Props, Model, Key>;
44
+ readonly view: ComponentType<Props & {
45
+ model: ReactiveModel<Model>;
46
+ }>;
47
+ };
48
+ //#endregion
49
+ //#region lib/component.d.ts
50
+ declare function component<Props, Model extends object>(config: ComponentConfig<Props, Model>): FC<Props>;
51
+ declare function component<Props, Key, Model extends object>(config: CachedComponentConfig<Props, Key, Model>): FC<Props>;
52
+ //#endregion
53
+ //#region lib/model-cache.d.ts
54
+ declare function createModelCache<Key, Props, Model extends object>(): ModelCache<Key, Props, Model>;
55
+ //#endregion
56
+ //#region lib/scope.d.ts
57
+ declare function ScopeProvider(props: {
58
+ scope: Scope;
59
+ children?: ReactNode;
60
+ }): ReactNode;
61
+ declare function useProvidedScope(): Scope;
62
+ //#endregion
63
+ //#region lib/use-model.d.ts
64
+ declare function useModel<Model extends object>(model: Model): ReactiveModel<Model>;
65
+ declare function useModel<Props, Model extends object>(factory: ModelFactory<Props, Model>, props: Props): ReactiveModel<Model>;
66
+ declare function useModel<Props, Key, Model extends object>(factory: ModelFactory<Props, Model, Key>, props: Props, options: CacheOptions<Props, Key, Model>): ReactiveModel<Model>;
67
+ //#endregion
68
+ //#region lib/use-unit.d.ts
69
+ declare function useUnit<Unit extends UnitLike>(unit: Unit): UnitValue<Unit>;
70
+ declare function useUnit<Shape extends readonly UnitLike[]>(shape: Shape): UnitShape<Shape>;
71
+ declare function useUnit<Shape extends Record<string, UnitLike>>(shape: Shape): UnitShape<Shape>;
72
+ //#endregion
73
+ export { type ModelCache, type ModelContext, type ModelFactory, type ModelInstance, type ReactiveModel, ScopeProvider, type UnitLike, type UnitValue, component, createModelCache, useModel, useProvidedScope, useUnit };
package/dist/index.mjs ADDED
@@ -0,0 +1,269 @@
1
+ import { createContext, createElement, useCallback, useContext, useEffect, useLayoutEffect, useMemo, useRef, useSyncExternalStore } from "react";
2
+ import { event, owner, run, scoped, store } from "@virentia/core";
3
+ //#region lib/model-cache.ts
4
+ function createModelCache() {
5
+ const byScope = /* @__PURE__ */ new WeakMap();
6
+ const maps = /* @__PURE__ */ new Set();
7
+ const getScopeMap = (scope) => {
8
+ let map = byScope.get(scope);
9
+ if (!map) {
10
+ map = /* @__PURE__ */ new Map();
11
+ byScope.set(scope, map);
12
+ maps.add(map);
13
+ }
14
+ return map;
15
+ };
16
+ const find = (key, scope) => {
17
+ if (scope) return byScope.get(scope)?.get(key);
18
+ for (const map of maps) {
19
+ const instance = map.get(key);
20
+ if (instance) return instance;
21
+ }
22
+ };
23
+ return {
24
+ has(key, scope) {
25
+ return Boolean(find(key, scope));
26
+ },
27
+ get(key, scope) {
28
+ return find(key, scope)?.model;
29
+ },
30
+ getInstance(key, scope) {
31
+ return find(key, scope);
32
+ },
33
+ delete(key, scope) {
34
+ if (scope) {
35
+ const map = byScope.get(scope);
36
+ const instance = map?.get(key);
37
+ if (!instance) return false;
38
+ instance.dispose();
39
+ map?.delete(key);
40
+ return true;
41
+ }
42
+ let deleted = false;
43
+ for (const map of maps) {
44
+ const instance = map.get(key);
45
+ if (!instance) continue;
46
+ instance.dispose();
47
+ map.delete(key);
48
+ deleted = true;
49
+ }
50
+ return deleted;
51
+ },
52
+ clear(scope) {
53
+ if (scope) {
54
+ const map = byScope.get(scope);
55
+ if (!map) return;
56
+ for (const instance of map.values()) instance.dispose();
57
+ map.clear();
58
+ return;
59
+ }
60
+ for (const map of maps) {
61
+ for (const instance of map.values()) instance.dispose();
62
+ map.clear();
63
+ }
64
+ },
65
+ [modelCacheInternal](scope, key, create) {
66
+ const map = getScopeMap(scope);
67
+ let instance = map.get(key);
68
+ if (!instance) {
69
+ instance = create();
70
+ map.set(key, instance);
71
+ }
72
+ return instance;
73
+ }
74
+ };
75
+ }
76
+ function getOrCreateCachedInstance(cache, scope, key, create) {
77
+ const internal = cache[modelCacheInternal];
78
+ if (!internal) throw new Error("[useModel] Unsupported model cache. Use createModelCache().");
79
+ return internal(scope, key, create);
80
+ }
81
+ const modelCacheInternal = Symbol("virentia.react.modelCacheInternal");
82
+ //#endregion
83
+ //#region lib/scope.ts
84
+ const ScopeContext = createContext(null);
85
+ function ScopeProvider(props) {
86
+ return createElement(ScopeContext.Provider, { value: props.scope }, props.children);
87
+ }
88
+ function useProvidedScope() {
89
+ const scope = useContext(ScopeContext);
90
+ if (!scope) throw new Error("[useProvidedScope] Scope is not provided. Wrap your tree with ScopeProvider.");
91
+ return scope;
92
+ }
93
+ //#endregion
94
+ //#region lib/utils.ts
95
+ const useIsomorphicLayoutEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
96
+ function readStore(unit, scope) {
97
+ return scoped(scope, () => {
98
+ const keys = Reflect.ownKeys(unit).filter((key) => !nativeStoreKeys.has(key));
99
+ if (keys.length === 1 && keys[0] === "value") return Reflect.get(unit, "value");
100
+ if (isArraySnapshot(unit, keys)) {
101
+ const length = Reflect.get(unit, "length");
102
+ return Array.from({ length }, (_value, index) => Reflect.get(unit, String(index)));
103
+ }
104
+ return Object.fromEntries(keys.map((key) => [key, Reflect.get(unit, key)]));
105
+ });
106
+ }
107
+ function writeStore(unit, value, scope) {
108
+ run({
109
+ unit: unit.node,
110
+ payload: value,
111
+ scope
112
+ });
113
+ }
114
+ function isUnitLike(value) {
115
+ return isStoreUnit(value) || isCallableUnit(value);
116
+ }
117
+ function isStoreUnit(value) {
118
+ return Boolean(value && (typeof value === "object" || typeof value === "function") && "node" in value && "subscribe" in value && typeof value.subscribe === "function");
119
+ }
120
+ function isCallableUnit(value) {
121
+ return Boolean(typeof value === "function" && "node" in value);
122
+ }
123
+ function isPlainObject(value) {
124
+ if (!value || typeof value !== "object") return false;
125
+ const prototype = Object.getPrototypeOf(value);
126
+ return prototype === Object.prototype || prototype === null;
127
+ }
128
+ function getComponentName(view) {
129
+ return `Virentia(${view.displayName ?? view.name ?? "Component"})`;
130
+ }
131
+ function isArraySnapshot(unit, keys) {
132
+ return keys.includes("length") && keys.every((key) => key === "length" || typeof key === "string" && arrayIndexPattern.test(key)) && typeof Reflect.get(unit, "length") === "number";
133
+ }
134
+ const arrayIndexPattern = /^(0|[1-9]\d*)$/;
135
+ const nativeStoreKeys = new Set([
136
+ "node",
137
+ "writable",
138
+ "subscribe",
139
+ "map",
140
+ "filter",
141
+ "filterMap"
142
+ ]);
143
+ //#endregion
144
+ //#region lib/use-unit.ts
145
+ function useUnit(input) {
146
+ return useUnitWithScope(input, useProvidedScope());
147
+ }
148
+ function useUnitWithScope(input, scope) {
149
+ if (Array.isArray(input)) return input.map((unit) => useSingleUnit(unit, scope));
150
+ if (isUnitLike(input)) return useSingleUnit(input, scope);
151
+ const result = {};
152
+ for (const key of Object.keys(input)) result[key] = useSingleUnit(input[key], scope);
153
+ return result;
154
+ }
155
+ function useSingleUnit(unit, scope) {
156
+ if (isStoreUnit(unit)) return useStoreUnit(unit, scope);
157
+ return useCallback((...args) => scoped(scope, () => unit(...args)), [scope, unit]);
158
+ }
159
+ function useStoreUnit(unit, scope) {
160
+ const snapshotRef = useRef(readStore(unit, scope));
161
+ snapshotRef.current = readStore(unit, scope);
162
+ const subscribe = useCallback((notify) => {
163
+ const unsubscribe = unit.subscribe((_value, nextScope) => {
164
+ if (nextScope !== scope) return;
165
+ snapshotRef.current = readStore(unit, scope);
166
+ notify();
167
+ });
168
+ snapshotRef.current = readStore(unit, scope);
169
+ return unsubscribe;
170
+ }, [scope, unit]);
171
+ const getSnapshot = useCallback(() => snapshotRef.current, []);
172
+ return useSyncExternalStore(subscribe, getSnapshot, getSnapshot);
173
+ }
174
+ //#endregion
175
+ //#region lib/use-model.ts
176
+ function useModel(modelOrFactory, props, options) {
177
+ const scope = useProvidedScope();
178
+ if (typeof modelOrFactory !== "function") return useReactiveModel(modelOrFactory, scope);
179
+ return useReactiveModel(useModelInstance(modelOrFactory, props, scope, options).model, scope);
180
+ }
181
+ function useModelInstance(factory, props, scope, options) {
182
+ const cache = options?.cache;
183
+ const key = options?.key;
184
+ const cached = Boolean(cache);
185
+ const instance = useMemo(() => {
186
+ if (cache) return getOrCreateCachedInstance(cache, scope, key, () => createModelInstance(factory, props, scope, key));
187
+ return createModelInstance(factory, props, scope, void 0);
188
+ }, [
189
+ cache,
190
+ key,
191
+ scope
192
+ ]);
193
+ useIsomorphicLayoutEffect(() => {
194
+ writeStore(instance.props, props, scope);
195
+ }, [
196
+ instance,
197
+ props,
198
+ scope
199
+ ]);
200
+ useEffect(() => {
201
+ scoped(scope, () => {
202
+ instance.mounts.value += 1;
203
+ instance.mounted();
204
+ });
205
+ return () => {
206
+ scoped(scope, () => {
207
+ instance.mounts.value = Math.max(0, instance.mounts.value - 1);
208
+ instance.unmounted();
209
+ });
210
+ if (!cached) instance.dispose();
211
+ };
212
+ }, [
213
+ cached,
214
+ instance,
215
+ scope
216
+ ]);
217
+ return instance;
218
+ }
219
+ function createModelInstance(factory, props, scope, key) {
220
+ return owner((dispose, modelOwner) => {
221
+ const context = {
222
+ scope,
223
+ owner: modelOwner,
224
+ props: store(props),
225
+ mounted: event(),
226
+ unmounted: event(),
227
+ mounts: store(0),
228
+ key
229
+ };
230
+ const model = scoped(scope, () => factory(context));
231
+ return {
232
+ ...context,
233
+ model,
234
+ dispose
235
+ };
236
+ });
237
+ }
238
+ function useReactiveModel(model, scope) {
239
+ const result = {};
240
+ for (const key of Reflect.ownKeys(model)) {
241
+ if (key === "dispose" || key === disposeSymbol) continue;
242
+ result[key] = useModelValue(Reflect.get(model, key), scope);
243
+ }
244
+ return result;
245
+ }
246
+ const disposeSymbol = typeof Symbol.dispose === "symbol" ? Symbol.dispose : Symbol.for("Symbol.dispose");
247
+ function useModelValue(value, scope) {
248
+ if (isUnitLike(value)) return useUnitWithScope(value, scope);
249
+ if (isPlainObject(value)) return useReactiveModel(value, scope);
250
+ return value;
251
+ }
252
+ //#endregion
253
+ //#region lib/component.ts
254
+ function component(config) {
255
+ const VirentiaComponent = (props) => {
256
+ const model = "cache" in config ? useModel(config.model, props, {
257
+ cache: config.cache,
258
+ key: config.key(props)
259
+ }) : useModel(config.model, props);
260
+ return createElement(config.view, {
261
+ ...props,
262
+ model
263
+ });
264
+ };
265
+ VirentiaComponent.displayName = getComponentName(config.view);
266
+ return VirentiaComponent;
267
+ }
268
+ //#endregion
269
+ export { ScopeProvider, component, createModelCache, useModel, useProvidedScope, useUnit };
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@virentia/react",
3
+ "version": "0.1.0",
4
+ "description": "",
5
+ "keywords": [],
6
+ "homepage": "https://movpushmov.dev/virentia/react/",
7
+ "bugs": {
8
+ "url": "https://github.com/movpushmov/virentia/issues"
9
+ },
10
+ "license": "MIT",
11
+ "author": "movpushmov",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/movpushmov/virentia.git",
15
+ "directory": "packages/react"
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "type": "module",
21
+ "main": "./dist/index.cjs",
22
+ "module": "./dist/index.mjs",
23
+ "types": "./dist/index.d.mts",
24
+ "exports": {
25
+ ".": {
26
+ "types": "./dist/index.d.mts",
27
+ "import": "./dist/index.mjs",
28
+ "require": "./dist/index.cjs"
29
+ }
30
+ },
31
+ "publishConfig": {
32
+ "access": "public"
33
+ },
34
+ "dependencies": {
35
+ "@virentia/core": "0.1.0"
36
+ },
37
+ "devDependencies": {
38
+ "@types/react": "^19.2.7",
39
+ "react": "^19.2.1"
40
+ },
41
+ "peerDependencies": {
42
+ "react": ">=18"
43
+ },
44
+ "scripts": {
45
+ "build": "tsdown"
46
+ }
47
+ }