@arcote.tech/arc-react 0.3.3 → 0.4.1
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/index.js +183 -63
- package/dist/src/factories/index.d.ts +0 -2
- package/dist/src/factories/model-provider-factory.d.ts +25 -2
- package/dist/src/form/form.d.ts +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/react-model.d.ts +2 -4
- package/package.json +5 -5
- package/dist/src/factories/use-commands-factory.d.ts +0 -3
- package/dist/src/factories/use-query-factory.d.ts +0 -3
package/dist/index.js
CHANGED
|
@@ -304,9 +304,19 @@ import {
|
|
|
304
304
|
Model,
|
|
305
305
|
QueryWire,
|
|
306
306
|
StreamingEventPublisher,
|
|
307
|
-
StreamingQueryCache
|
|
307
|
+
StreamingQueryCache,
|
|
308
|
+
buildContextAccessor,
|
|
309
|
+
resolveQueryChange
|
|
308
310
|
} from "@arcote.tech/arc";
|
|
309
|
-
import {
|
|
311
|
+
import {
|
|
312
|
+
createContext as createContext4,
|
|
313
|
+
useContext as useContext3,
|
|
314
|
+
useEffect as useEffect2,
|
|
315
|
+
useLayoutEffect,
|
|
316
|
+
useMemo as useMemo3,
|
|
317
|
+
useRef,
|
|
318
|
+
useState as useState3
|
|
319
|
+
} from "react";
|
|
310
320
|
import { jsx as jsx5 } from "react/jsx-runtime";
|
|
311
321
|
var modelProviderFactory = (context, options) => {
|
|
312
322
|
const ModelContext = createContext4(null);
|
|
@@ -314,10 +324,21 @@ var modelProviderFactory = (context, options) => {
|
|
|
314
324
|
const eventWire = options.remoteUrl ? new EventWire(options.remoteUrl) : undefined;
|
|
315
325
|
const queryWire = options.remoteUrl ? new QueryWire(options.remoteUrl) : undefined;
|
|
316
326
|
const authAdapter = new AuthAdapter;
|
|
327
|
+
authAdapter.loadPersisted();
|
|
317
328
|
let initialized = false;
|
|
318
329
|
let cachedModel = null;
|
|
319
330
|
let cachedDataStorage = null;
|
|
320
331
|
let onResetCallback = null;
|
|
332
|
+
let needsReconnect = false;
|
|
333
|
+
const scopeTokenListeners = new Map;
|
|
334
|
+
function notifyScopeTokenListeners(scope) {
|
|
335
|
+
const listeners = scopeTokenListeners.get(scope);
|
|
336
|
+
if (listeners) {
|
|
337
|
+
for (const listener of listeners) {
|
|
338
|
+
listener();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
321
342
|
function ModelProvider(props) {
|
|
322
343
|
const [model, setModel] = useState3(cachedModel);
|
|
323
344
|
const [resetTrigger, setResetTrigger] = useState3(0);
|
|
@@ -333,6 +354,10 @@ var modelProviderFactory = (context, options) => {
|
|
|
333
354
|
useEffect2(() => {
|
|
334
355
|
if (initialized && cachedModel) {
|
|
335
356
|
setModel(cachedModel);
|
|
357
|
+
if (needsReconnect && eventWire && authAdapter.isAuthenticated()) {
|
|
358
|
+
eventWire.connect();
|
|
359
|
+
needsReconnect = false;
|
|
360
|
+
}
|
|
336
361
|
return;
|
|
337
362
|
}
|
|
338
363
|
initialized = true;
|
|
@@ -450,13 +475,12 @@ Event payload:`, event.payload);
|
|
|
450
475
|
}
|
|
451
476
|
if (cancelled)
|
|
452
477
|
return;
|
|
453
|
-
const modelEventWire = options.dbAdapterFactory ? eventWire : undefined;
|
|
454
478
|
const newModel = new Model(context, {
|
|
455
479
|
adapters: {
|
|
456
480
|
dataStorage,
|
|
457
481
|
commandWire,
|
|
458
482
|
eventPublisher,
|
|
459
|
-
eventWire
|
|
483
|
+
eventWire,
|
|
460
484
|
authAdapter,
|
|
461
485
|
queryWire,
|
|
462
486
|
streamingCache
|
|
@@ -476,7 +500,10 @@ Event payload:`, event.payload);
|
|
|
476
500
|
initializeModel();
|
|
477
501
|
return () => {
|
|
478
502
|
cancelled = true;
|
|
479
|
-
eventWire
|
|
503
|
+
if (eventWire) {
|
|
504
|
+
needsReconnect = true;
|
|
505
|
+
eventWire.disconnect();
|
|
506
|
+
}
|
|
480
507
|
};
|
|
481
508
|
}, [resetTrigger]);
|
|
482
509
|
if (!model)
|
|
@@ -493,16 +520,153 @@ Event payload:`, event.payload);
|
|
|
493
520
|
}
|
|
494
521
|
return model;
|
|
495
522
|
}
|
|
496
|
-
function
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
523
|
+
function syncPersistedToken(model, scopeName) {
|
|
524
|
+
const scoped = model.scope(scopeName);
|
|
525
|
+
const persisted = authAdapter.getToken(scopeName);
|
|
526
|
+
if (persisted && scoped.getToken() !== persisted) {
|
|
527
|
+
scoped.setToken(persisted);
|
|
528
|
+
}
|
|
529
|
+
return scoped;
|
|
530
|
+
}
|
|
531
|
+
function createScope(name) {
|
|
532
|
+
function setToken(token) {
|
|
533
|
+
authAdapter.setToken(token, name);
|
|
534
|
+
if (cachedModel) {
|
|
535
|
+
cachedModel.scope(name).setToken(token);
|
|
536
|
+
} else {
|
|
537
|
+
eventWire?.setScopeToken(name, token);
|
|
538
|
+
if (eventWire && token && eventWire.getState() === "disconnected") {
|
|
539
|
+
eventWire.connect();
|
|
540
|
+
}
|
|
504
541
|
}
|
|
542
|
+
notifyScopeTokenListeners(name);
|
|
543
|
+
}
|
|
544
|
+
function useToken() {
|
|
545
|
+
const [token, setTokenState] = useState3(() => authAdapter.getToken(name));
|
|
546
|
+
useEffect2(() => {
|
|
547
|
+
if (!scopeTokenListeners.has(name)) {
|
|
548
|
+
scopeTokenListeners.set(name, new Set);
|
|
549
|
+
}
|
|
550
|
+
const listeners = scopeTokenListeners.get(name);
|
|
551
|
+
const listener = () => setTokenState(authAdapter.getToken(name));
|
|
552
|
+
listeners.add(listener);
|
|
553
|
+
return () => {
|
|
554
|
+
listeners.delete(listener);
|
|
555
|
+
};
|
|
556
|
+
}, []);
|
|
557
|
+
return token;
|
|
558
|
+
}
|
|
559
|
+
const ScopeContext = createContext4(null);
|
|
560
|
+
function Provider(props) {
|
|
561
|
+
return /* @__PURE__ */ jsx5(ScopeContext.Provider, {
|
|
562
|
+
value: name,
|
|
563
|
+
children: props.children
|
|
564
|
+
}, undefined, false, undefined, this);
|
|
565
|
+
}
|
|
566
|
+
function useQuery() {
|
|
567
|
+
const model = useModel();
|
|
568
|
+
const token = useToken();
|
|
569
|
+
const [data, setData] = useState3(undefined);
|
|
570
|
+
const [loading, setLoading] = useState3(true);
|
|
571
|
+
const descriptorRef = useRef(null);
|
|
572
|
+
const [subKey, setSubKey] = useState3("");
|
|
573
|
+
const scoped = syncPersistedToken(model, name);
|
|
574
|
+
const adapters = scoped.getAdapters();
|
|
575
|
+
useLayoutEffect(() => {
|
|
576
|
+
const desc = descriptorRef.current;
|
|
577
|
+
if (!desc)
|
|
578
|
+
return;
|
|
579
|
+
const key = JSON.stringify({ ...desc, token });
|
|
580
|
+
if (key !== subKey)
|
|
581
|
+
setSubKey(key);
|
|
582
|
+
});
|
|
583
|
+
useEffect2(() => {
|
|
584
|
+
const desc = descriptorRef.current;
|
|
585
|
+
if (!desc || !subKey)
|
|
586
|
+
return;
|
|
587
|
+
const unsubs = [];
|
|
588
|
+
const element = model.context.get(desc.element);
|
|
589
|
+
const qCtx = element?.queryContext?.(adapters);
|
|
590
|
+
const method = qCtx?.[desc.method];
|
|
591
|
+
const reExecute = async () => {
|
|
592
|
+
if (!method)
|
|
593
|
+
return;
|
|
594
|
+
try {
|
|
595
|
+
const result = await method(...desc.args);
|
|
596
|
+
setData(result);
|
|
597
|
+
setLoading(false);
|
|
598
|
+
} catch (err) {
|
|
599
|
+
console.error(`[Arc] Query error:`, err);
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
if (adapters.streamingCache) {
|
|
603
|
+
const store = adapters.streamingCache.getStore(desc.element);
|
|
604
|
+
let cachedResult;
|
|
605
|
+
unsubs.push(store.subscribe((events) => {
|
|
606
|
+
if (!events) {
|
|
607
|
+
cachedResult = undefined;
|
|
608
|
+
reExecute();
|
|
609
|
+
return;
|
|
610
|
+
}
|
|
611
|
+
if (cachedResult !== undefined) {
|
|
612
|
+
let current = cachedResult;
|
|
613
|
+
let changed = false;
|
|
614
|
+
for (const event of events) {
|
|
615
|
+
const newResult = resolveQueryChange(current, event, desc.args[0] ?? {});
|
|
616
|
+
if (newResult !== false) {
|
|
617
|
+
current = newResult;
|
|
618
|
+
changed = true;
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
if (!changed)
|
|
622
|
+
return;
|
|
623
|
+
cachedResult = current;
|
|
624
|
+
reExecute();
|
|
625
|
+
return;
|
|
626
|
+
}
|
|
627
|
+
reExecute();
|
|
628
|
+
}));
|
|
629
|
+
unsubs.push(adapters.streamingCache.subscribeQuery(desc, adapters.eventWire, name));
|
|
630
|
+
if (store.hasData()) {
|
|
631
|
+
reExecute();
|
|
632
|
+
}
|
|
633
|
+
} else {
|
|
634
|
+
if (adapters.eventPublisher) {
|
|
635
|
+
const eventTypes = element?.getElements?.()?.map((e) => e.name) ?? [];
|
|
636
|
+
if (eventTypes.length > 0) {
|
|
637
|
+
for (const eventType of eventTypes) {
|
|
638
|
+
unsubs.push(adapters.eventPublisher.subscribe(eventType, () => reExecute()));
|
|
639
|
+
}
|
|
640
|
+
} else {
|
|
641
|
+
unsubs.push(adapters.eventPublisher.subscribe("*", () => reExecute()));
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
reExecute();
|
|
645
|
+
}
|
|
646
|
+
return () => {
|
|
647
|
+
for (const unsub of unsubs)
|
|
648
|
+
unsub();
|
|
649
|
+
};
|
|
650
|
+
}, [subKey]);
|
|
651
|
+
return useMemo3(() => buildContextAccessor(model.context, adapters, "queryContext", (desc, _execute) => {
|
|
652
|
+
descriptorRef.current = desc;
|
|
653
|
+
return [data, loading];
|
|
654
|
+
}), [model, token, data, loading]);
|
|
655
|
+
}
|
|
656
|
+
function useMutation() {
|
|
657
|
+
const model = useModel();
|
|
658
|
+
return useMemo3(() => {
|
|
659
|
+
const scoped = syncPersistedToken(model, name);
|
|
660
|
+
return buildContextAccessor(model.context, scoped.getAdapters(), "mutateContext", (_descriptor, execute) => execute());
|
|
661
|
+
}, [model]);
|
|
505
662
|
}
|
|
663
|
+
return {
|
|
664
|
+
Provider,
|
|
665
|
+
useQuery,
|
|
666
|
+
useMutation,
|
|
667
|
+
setToken,
|
|
668
|
+
useToken
|
|
669
|
+
};
|
|
506
670
|
}
|
|
507
671
|
async function resetModel() {
|
|
508
672
|
eventWire?.disconnect();
|
|
@@ -512,72 +676,28 @@ Event payload:`, event.payload);
|
|
|
512
676
|
}
|
|
513
677
|
cachedModel = null;
|
|
514
678
|
initialized = false;
|
|
515
|
-
authAdapter.
|
|
516
|
-
commandWire?.setAuthToken(null);
|
|
517
|
-
queryWire?.setAuthToken(null);
|
|
518
|
-
eventWire?.setAuthToken(null);
|
|
679
|
+
authAdapter.clear();
|
|
519
680
|
if (onResetCallback) {
|
|
520
681
|
onResetCallback();
|
|
521
682
|
}
|
|
522
683
|
}
|
|
523
|
-
function onReset(callback) {
|
|
524
|
-
onResetCallback = callback;
|
|
525
|
-
}
|
|
526
684
|
return {
|
|
527
685
|
ModelProvider,
|
|
528
686
|
useModel,
|
|
529
687
|
commandWire,
|
|
530
688
|
eventWire,
|
|
531
|
-
|
|
532
|
-
resetModel
|
|
533
|
-
onReset
|
|
534
|
-
};
|
|
535
|
-
};
|
|
536
|
-
// src/factories/use-commands-factory.tsx
|
|
537
|
-
import {
|
|
538
|
-
mutationExecutor
|
|
539
|
-
} from "@arcote.tech/arc";
|
|
540
|
-
import { useMemo as useMemo3 } from "react";
|
|
541
|
-
var useCommandsFactory = (useModel) => {
|
|
542
|
-
return function useCommands() {
|
|
543
|
-
const model = useModel();
|
|
544
|
-
return useMemo3(() => mutationExecutor(model), [model]);
|
|
545
|
-
};
|
|
546
|
-
};
|
|
547
|
-
// src/factories/use-query-factory.tsx
|
|
548
|
-
import {
|
|
549
|
-
liveQuery
|
|
550
|
-
} from "@arcote.tech/arc";
|
|
551
|
-
import { useEffect as useEffect3, useState as useState4 } from "react";
|
|
552
|
-
var useQueryFactory = (useModel) => {
|
|
553
|
-
return function useQuery(queryFn, dependencies = []) {
|
|
554
|
-
const model = useModel();
|
|
555
|
-
const [loading, setLoading] = useState4(true);
|
|
556
|
-
const [result, setResult] = useState4(undefined);
|
|
557
|
-
useEffect3(() => {
|
|
558
|
-
const { unsubscribe } = liveQuery(model, queryFn, (newResult) => {
|
|
559
|
-
setResult(newResult);
|
|
560
|
-
setLoading(false);
|
|
561
|
-
});
|
|
562
|
-
return () => {
|
|
563
|
-
unsubscribe();
|
|
564
|
-
};
|
|
565
|
-
}, [model, ...dependencies]);
|
|
566
|
-
return [result, loading];
|
|
689
|
+
createScope,
|
|
690
|
+
resetModel
|
|
567
691
|
};
|
|
568
692
|
};
|
|
569
693
|
// src/react-model.tsx
|
|
570
694
|
var reactModel = (context, options = {}) => {
|
|
571
|
-
const { ModelProvider, useModel, commandWire,
|
|
572
|
-
const useQuery = useQueryFactory(useModel);
|
|
573
|
-
const useCommands = useCommandsFactory(useModel);
|
|
695
|
+
const { ModelProvider, useModel, commandWire, createScope, resetModel } = modelProviderFactory(context, options);
|
|
574
696
|
return {
|
|
575
697
|
ModelProvider,
|
|
576
698
|
useModel,
|
|
577
|
-
|
|
578
|
-
useCommands,
|
|
699
|
+
createScope,
|
|
579
700
|
commandWire,
|
|
580
|
-
setAuthToken,
|
|
581
701
|
resetModel
|
|
582
702
|
};
|
|
583
703
|
};
|
|
@@ -596,4 +716,4 @@ export {
|
|
|
596
716
|
Form
|
|
597
717
|
};
|
|
598
718
|
|
|
599
|
-
//# debugId=
|
|
719
|
+
//# debugId=075760FF00A2465564756E2164756E21
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import { CommandWire, EventWire, Model, type ArcContextAny } from "@arcote.tech/arc";
|
|
2
2
|
import type { ReactModelOptions } from "../options";
|
|
3
|
+
/**
|
|
4
|
+
* Accessor type for useQuery() — maps element names to their query methods,
|
|
5
|
+
* where each method returns [data, loading] tuple.
|
|
6
|
+
*/
|
|
7
|
+
export type QueryAccessor<C extends ArcContextAny> = {
|
|
8
|
+
[E in C["elements"][number] as E["queryContext"] extends (...args: any[]) => any ? E["name"] : never]: E["queryContext"] extends (...args: any[]) => infer R ? {
|
|
9
|
+
[K in keyof R]: R[K] extends (...args: infer A) => Promise<infer V> ? (...args: A) => readonly [V | undefined, boolean] : never;
|
|
10
|
+
} : never;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Accessor type for useMutation() — maps element names to their mutation methods.
|
|
14
|
+
*/
|
|
15
|
+
export type MutationAccessor<C extends ArcContextAny> = {
|
|
16
|
+
[E in C["elements"][number] as E["mutateContext"] extends (...args: any[]) => any ? E["name"] : never]: E["mutateContext"] extends (...args: any[]) => infer R ? R : never;
|
|
17
|
+
};
|
|
18
|
+
export interface ScopeAPI<C extends ArcContextAny = ArcContextAny> {
|
|
19
|
+
Provider: React.FC<{
|
|
20
|
+
children: React.ReactNode;
|
|
21
|
+
}>;
|
|
22
|
+
useQuery: () => QueryAccessor<C>;
|
|
23
|
+
useMutation: () => MutationAccessor<C>;
|
|
24
|
+
setToken: (token: string | null) => void;
|
|
25
|
+
useToken: () => string | null;
|
|
26
|
+
}
|
|
3
27
|
export declare const modelProviderFactory: <C extends ArcContextAny>(context: C, options: ReactModelOptions) => {
|
|
4
28
|
ModelProvider: (props: {
|
|
5
29
|
children: React.ReactNode;
|
|
@@ -7,8 +31,7 @@ export declare const modelProviderFactory: <C extends ArcContextAny>(context: C,
|
|
|
7
31
|
useModel: () => Model<C>;
|
|
8
32
|
commandWire: CommandWire | undefined;
|
|
9
33
|
eventWire: EventWire | undefined;
|
|
10
|
-
|
|
34
|
+
createScope: (name: string) => ScopeAPI<C>;
|
|
11
35
|
resetModel: () => Promise<void>;
|
|
12
|
-
onReset: (callback: () => void) => void;
|
|
13
36
|
};
|
|
14
37
|
//# sourceMappingURL=model-provider-factory.d.ts.map
|
package/dist/src/form/form.d.ts
CHANGED
|
@@ -28,5 +28,5 @@ export type FormProps<T extends ArcObjectAny> = {
|
|
|
28
28
|
};
|
|
29
29
|
export declare const FormContext: React.Context<FormContextValue<any> | null>;
|
|
30
30
|
export declare function useForm<T extends ArcObjectAny>(): FormContextValue<T>;
|
|
31
|
-
export declare const Form: <T extends ArcObjectAny>(props: FormProps<T> & React.RefAttributes<FormRef<T>>) => JSX.Element;
|
|
31
|
+
export declare const Form: <T extends ArcObjectAny>(props: FormProps<T> & React.RefAttributes<FormRef<T>>) => React.JSX.Element;
|
|
32
32
|
//# sourceMappingURL=form.d.ts.map
|
package/dist/src/index.d.ts
CHANGED
|
@@ -5,12 +5,10 @@ export declare const reactModel: <C extends ArcContextAny>(context: C, options?:
|
|
|
5
5
|
children: React.ReactNode;
|
|
6
6
|
}) => import("react/jsx-dev-runtime").JSX.Element | undefined;
|
|
7
7
|
readonly useModel: () => import("@arcote.tech/arc").Model<C>;
|
|
8
|
-
|
|
9
|
-
readonly
|
|
8
|
+
/** Create a named scope with its own token, Provider, and hooks */
|
|
9
|
+
readonly createScope: (name: string) => import("./factories").ScopeAPI<C>;
|
|
10
10
|
/** CommandWire instance for remote execution (if remoteUrl provided) */
|
|
11
11
|
readonly commandWire: import("@arcote.tech/arc").CommandWire | undefined;
|
|
12
|
-
/** Set auth token for remote command execution */
|
|
13
|
-
readonly setAuthToken: (token: string | null) => void;
|
|
14
12
|
/** Reset model - destroys local database and disconnects from host */
|
|
15
13
|
readonly resetModel: () => Promise<void>;
|
|
16
14
|
};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@arcote.tech/arc-react",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.4.1",
|
|
5
5
|
"private": false,
|
|
6
6
|
"author": "Przemysław Krasiński [arcote.tech]",
|
|
7
7
|
"description": "React client for the Arc framework, providing utilities for querying data and executing commands, enhancing the development of reactive and efficient user interfaces.",
|
|
@@ -24,8 +24,8 @@
|
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
26
|
"@types/bun": "latest",
|
|
27
|
-
"@types/react": "^
|
|
28
|
-
"@types/react-dom": "^
|
|
27
|
+
"@types/react": "^19.2.7",
|
|
28
|
+
"@types/react-dom": "^19.2.3",
|
|
29
29
|
"nodemon": "^2.0.22",
|
|
30
30
|
"prettier": "^3.0.3",
|
|
31
31
|
"rimraf": "^5.0.5",
|
|
@@ -33,8 +33,8 @@
|
|
|
33
33
|
},
|
|
34
34
|
"peerDependencies": {
|
|
35
35
|
"@arcote.tech/arc": "latest",
|
|
36
|
-
"react": "^18.0.0",
|
|
37
|
-
"react-dom": "^18.0.0",
|
|
36
|
+
"react": "^18.0.0 || ^19.0.0",
|
|
37
|
+
"react-dom": "^18.0.0 || ^19.0.0",
|
|
38
38
|
"typescript": "^5.0.0"
|
|
39
39
|
},
|
|
40
40
|
"files": [
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import { type ArcContextAny, type Model } from "@arcote.tech/arc";
|
|
2
|
-
export declare const useCommandsFactory: <C extends ArcContextAny>(useModel: () => Model<C>) => () => { [Element in C["elements"][number] as Element["mutateContext"] extends (...args: any[]) => infer Return ? Element["name"] : never]: Element["mutateContext"] extends (...args: any[]) => infer Return ? Return : never; } extends infer T ? { [K in keyof T]: T[K]; } : never;
|
|
3
|
-
//# sourceMappingURL=use-commands-factory.d.ts.map
|
|
@@ -1,3 +0,0 @@
|
|
|
1
|
-
import { type ArcContextAny, type Model, type QueryContext } from "@arcote.tech/arc";
|
|
2
|
-
export declare const useQueryFactory: <C extends ArcContextAny>(useModel: () => Model<C>) => <TResult>(queryFn: (q: QueryContext<C>) => Promise<TResult>, dependencies?: any[]) => [TResult | undefined, boolean];
|
|
3
|
-
//# sourceMappingURL=use-query-factory.d.ts.map
|