@arcote.tech/arc-react 0.3.4 → 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 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 { createContext as createContext4, useContext as useContext3, useEffect as useEffect2, useState as useState3 } from "react";
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,11 +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;
321
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
+ }
322
342
  function ModelProvider(props) {
323
343
  const [model, setModel] = useState3(cachedModel);
324
344
  const [resetTrigger, setResetTrigger] = useState3(0);
@@ -455,13 +475,12 @@ Event payload:`, event.payload);
455
475
  }
456
476
  if (cancelled)
457
477
  return;
458
- const modelEventWire = options.dbAdapterFactory ? eventWire : undefined;
459
478
  const newModel = new Model(context, {
460
479
  adapters: {
461
480
  dataStorage,
462
481
  commandWire,
463
482
  eventPublisher,
464
- eventWire: modelEventWire,
483
+ eventWire,
465
484
  authAdapter,
466
485
  queryWire,
467
486
  streamingCache
@@ -501,16 +520,153 @@ Event payload:`, event.payload);
501
520
  }
502
521
  return model;
503
522
  }
504
- function setAuthToken(token) {
505
- authAdapter.setToken(token);
506
- commandWire?.setAuthToken(token);
507
- queryWire?.setAuthToken(token);
508
- if (eventWire) {
509
- eventWire.setAuthToken(token);
510
- if (token && eventWire.getState() === "disconnected") {
511
- eventWire.connect();
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
+ }
512
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);
513
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]);
662
+ }
663
+ return {
664
+ Provider,
665
+ useQuery,
666
+ useMutation,
667
+ setToken,
668
+ useToken
669
+ };
514
670
  }
515
671
  async function resetModel() {
516
672
  eventWire?.disconnect();
@@ -520,72 +676,28 @@ Event payload:`, event.payload);
520
676
  }
521
677
  cachedModel = null;
522
678
  initialized = false;
523
- authAdapter.setToken(null);
524
- commandWire?.setAuthToken(null);
525
- queryWire?.setAuthToken(null);
526
- eventWire?.setAuthToken(null);
679
+ authAdapter.clear();
527
680
  if (onResetCallback) {
528
681
  onResetCallback();
529
682
  }
530
683
  }
531
- function onReset(callback) {
532
- onResetCallback = callback;
533
- }
534
684
  return {
535
685
  ModelProvider,
536
686
  useModel,
537
687
  commandWire,
538
688
  eventWire,
539
- setAuthToken,
540
- resetModel,
541
- onReset
542
- };
543
- };
544
- // src/factories/use-commands-factory.tsx
545
- import {
546
- mutationExecutor
547
- } from "@arcote.tech/arc";
548
- import { useMemo as useMemo3 } from "react";
549
- var useCommandsFactory = (useModel) => {
550
- return function useCommands() {
551
- const model = useModel();
552
- return useMemo3(() => mutationExecutor(model), [model]);
553
- };
554
- };
555
- // src/factories/use-query-factory.tsx
556
- import {
557
- liveQuery
558
- } from "@arcote.tech/arc";
559
- import { useEffect as useEffect3, useState as useState4 } from "react";
560
- var useQueryFactory = (useModel) => {
561
- return function useQuery(queryFn, dependencies = []) {
562
- const model = useModel();
563
- const [loading, setLoading] = useState4(true);
564
- const [result, setResult] = useState4(undefined);
565
- useEffect3(() => {
566
- const { unsubscribe } = liveQuery(model, queryFn, (newResult) => {
567
- setResult(newResult);
568
- setLoading(false);
569
- });
570
- return () => {
571
- unsubscribe();
572
- };
573
- }, [model, ...dependencies]);
574
- return [result, loading];
689
+ createScope,
690
+ resetModel
575
691
  };
576
692
  };
577
693
  // src/react-model.tsx
578
694
  var reactModel = (context, options = {}) => {
579
- const { ModelProvider, useModel, commandWire, setAuthToken, resetModel } = modelProviderFactory(context, options);
580
- const useQuery = useQueryFactory(useModel);
581
- const useCommands = useCommandsFactory(useModel);
695
+ const { ModelProvider, useModel, commandWire, createScope, resetModel } = modelProviderFactory(context, options);
582
696
  return {
583
697
  ModelProvider,
584
698
  useModel,
585
- useQuery,
586
- useCommands,
699
+ createScope,
587
700
  commandWire,
588
- setAuthToken,
589
701
  resetModel
590
702
  };
591
703
  };
@@ -604,4 +716,4 @@ export {
604
716
  Form
605
717
  };
606
718
 
607
- //# debugId=AB2860A19A07A53D64756E2164756E21
719
+ //# debugId=075760FF00A2465564756E2164756E21
@@ -1,4 +1,2 @@
1
1
  export * from "./model-provider-factory";
2
- export * from "./use-commands-factory";
3
- export * from "./use-query-factory";
4
2
  //# sourceMappingURL=index.d.ts.map
@@ -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
- setAuthToken: (token: string | null) => void;
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
@@ -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
@@ -1,4 +1,5 @@
1
1
  export * from "./form/";
2
2
  export * from "./options";
3
3
  export * from "./react-model";
4
+ export type { ScopeAPI } from "./factories/model-provider-factory";
4
5
  //# sourceMappingURL=index.d.ts.map
@@ -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
- readonly useQuery: <TResult>(queryFn: (q: { [Element in C["elements"][number] as Element["queryContext"] extends (...args: any[]) => infer Return ? Element["name"] : never]: Element["queryContext"] extends (...args: any[]) => infer Return ? Return : never; } extends infer T ? { [K in keyof T]: T[K]; } : never) => Promise<TResult>, dependencies?: any[]) => [TResult | undefined, boolean];
9
- readonly useCommands: () => { [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;
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.3.4",
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": "^18.3.5",
28
- "@types/react-dom": "^18.3.1",
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