@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 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,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: modelEventWire,
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?.disconnect();
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 setAuthToken(token) {
497
- authAdapter.setToken(token);
498
- commandWire?.setAuthToken(token);
499
- queryWire?.setAuthToken(token);
500
- if (eventWire) {
501
- eventWire.setAuthToken(token);
502
- if (token && eventWire.getState() === "disconnected") {
503
- 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
+ }
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.setToken(null);
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
- setAuthToken,
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, setAuthToken, resetModel } = modelProviderFactory(context, options);
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
- useQuery,
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=84D132984522BD8664756E2164756E21
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.3",
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