@actdim/dynstruct 1.2.8 → 1.2.9
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 +141 -287
- package/dist/services/ServiceProvider.d.ts +1 -1
- package/dist/services/ServiceProvider.d.ts.map +1 -1
- package/dist/services/ServiceProvider.es.js +1 -1
- package/dist/services/react/ServiceProvider.d.ts +1 -1
- package/dist/services/react/ServiceProvider.d.ts.map +1 -1
- package/dist/services/react/ServiceProvider.es.js +5 -5
- package/package.json +2 -2
- package/dist/componentModel/adapters.d.ts +0 -20
- package/dist/componentModel/adapters.d.ts.map +0 -1
- package/dist/componentModel/adapters.es.js +0 -33
- package/dist/componentModel/adapters.es.js.map +0 -1
package/README.md
CHANGED
|
@@ -2,11 +2,35 @@
|
|
|
2
2
|
|
|
3
3
|
Build scalable applications with dynamic structured components, explicit wiring, and decoupled message flow. Keep architecture clean and modular.
|
|
4
4
|
|
|
5
|
-
[](https://www.npmjs.com/package/@actdim/dynstruct)
|
|
6
|
-
[](https://www.typescriptlang.org/)
|
|
7
|
-
[](LICENSE)
|
|
8
|
-
|
|
9
|
-
##
|
|
5
|
+
[](https://www.npmjs.com/package/@actdim/dynstruct)
|
|
6
|
+
[](https://www.typescriptlang.org/)
|
|
7
|
+
[](LICENSE)
|
|
8
|
+
|
|
9
|
+
## Table of Contents
|
|
10
|
+
|
|
11
|
+
- [Overview](#overview)
|
|
12
|
+
- [Framework Support](#framework-support)
|
|
13
|
+
- [Features](#features)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [How It Works](#how-it-works)
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Getting Started (React)](#getting-started-react)
|
|
18
|
+
- [Key Advantages (React Examples)](#key-advantages-react-examples)
|
|
19
|
+
- [Core Concepts](#core-concepts)
|
|
20
|
+
- [More Examples (React)](#more-examples-react)
|
|
21
|
+
- [Architecture](#architecture)
|
|
22
|
+
- [API Reference](#api-reference)
|
|
23
|
+
- [Storybook Examples](#storybook-examples)
|
|
24
|
+
- [Development](#development)
|
|
25
|
+
- [Package Management](#package-management)
|
|
26
|
+
- [Contributing](#contributing)
|
|
27
|
+
- [License](#license)
|
|
28
|
+
- [Author](#author)
|
|
29
|
+
- [Repository](#repository)
|
|
30
|
+
- [Issues](#issues)
|
|
31
|
+
- [Keywords](#keywords)
|
|
32
|
+
|
|
33
|
+
## Overview
|
|
10
34
|
|
|
11
35
|
**@actdim/dynstruct** is a TypeScript-based component system and architectural framework for building large-scale, modular applications. It provides a structure-first, declarative approach to component design with:
|
|
12
36
|
|
|
@@ -73,13 +97,14 @@ The core pattern in dynstruct is **structure-first composition** where parent co
|
|
|
73
97
|
npm install @actdim/dynstruct
|
|
74
98
|
```
|
|
75
99
|
|
|
76
|
-
### Peer Dependencies
|
|
77
|
-
|
|
78
|
-
This package requires the following peer dependencies:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
100
|
+
### Peer Dependencies
|
|
101
|
+
|
|
102
|
+
This package requires the following peer dependencies:
|
|
103
|
+
For message bus functionality, install [@actdim/msgmesh](https://www.npmjs.com/package/@actdim/msgmesh).
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
npm install react react-dom mobx mobx-react-lite mobx-utils \
|
|
107
|
+
@actdim/msgmesh @actdim/utico react-router react-router-dom \
|
|
83
108
|
rxjs uuid path-to-regexp jwt-decode http-status
|
|
84
109
|
```
|
|
85
110
|
|
|
@@ -92,7 +117,7 @@ pnpm add @actdim/dynstruct @actdim/msgmesh @actdim/utico \
|
|
|
92
117
|
jwt-decode http-status
|
|
93
118
|
```
|
|
94
119
|
|
|
95
|
-
##
|
|
120
|
+
## Getting Started (React)
|
|
96
121
|
|
|
97
122
|
> **Note:** All examples below are for the **React** implementation. SolidJS and Vue.js versions will have similar structure with framework-specific adapters.
|
|
98
123
|
|
|
@@ -824,7 +849,7 @@ type Struct = ComponentStruct<
|
|
|
824
849
|
|
|
825
850
|
// List of effect names that will be available in this component.
|
|
826
851
|
// Effect implementations are defined in ComponentDef (see below).
|
|
827
|
-
effects: ['
|
|
852
|
+
effects: ['computeSummary', 'trackCounter'];
|
|
828
853
|
}
|
|
829
854
|
>;
|
|
830
855
|
```
|
|
@@ -870,23 +895,24 @@ const useMyComponent = (params: ComponentParams<Struct>) => {
|
|
|
870
895
|
},
|
|
871
896
|
|
|
872
897
|
effects: {
|
|
873
|
-
// Effect implementations. Effects are
|
|
874
|
-
//
|
|
875
|
-
//
|
|
876
|
-
// changes.
|
|
898
|
+
// Effect implementations. Effects are auto-tracking reactive
|
|
899
|
+
// functions that re-run automatically whenever any reactive
|
|
900
|
+
// property accessed inside them changes.
|
|
877
901
|
//
|
|
878
902
|
// Effects are accessed on the component instance by name via
|
|
879
|
-
// the `effects` property (e.g. c.effects.
|
|
903
|
+
// the `effects` property (e.g. c.effects.computeSummary).
|
|
880
904
|
//
|
|
881
905
|
// An effect runs immediately when the component is created and
|
|
882
906
|
// can later be manually paused, resumed, or stopped entirely.
|
|
883
|
-
|
|
884
|
-
|
|
907
|
+
computeSummary: (component) => {
|
|
908
|
+
// Re-runs whenever m.items changes
|
|
909
|
+
m.message = `Total items: ${m.items.length}`;
|
|
885
910
|
// Return an optional cleanup function
|
|
886
911
|
return () => { /* cleanup */ };
|
|
887
912
|
},
|
|
888
|
-
|
|
889
|
-
|
|
913
|
+
trackCounter: (component) => {
|
|
914
|
+
// Re-runs whenever m.counter changes
|
|
915
|
+
if (m.counter > 100) m.message = 'Counter is high!';
|
|
890
916
|
},
|
|
891
917
|
},
|
|
892
918
|
|
|
@@ -1719,18 +1745,18 @@ const useForm = (params: ComponentParams<FormStruct>) => {
|
|
|
1719
1745
|
isValid: false
|
|
1720
1746
|
},
|
|
1721
1747
|
events: {
|
|
1722
|
-
// Validate email
|
|
1723
|
-
onChangeEmail: (
|
|
1748
|
+
// Validate email after it changes — onChange receives only the new value
|
|
1749
|
+
onChangeEmail: (value) => {
|
|
1724
1750
|
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
1725
|
-
m.isValid = emailRegex.test(
|
|
1751
|
+
m.isValid = emailRegex.test(value) && m.password.length >= 6;
|
|
1726
1752
|
},
|
|
1727
1753
|
|
|
1728
|
-
// Validate password
|
|
1729
|
-
onChangePassword: (
|
|
1730
|
-
m.isValid = m.email.includes('@') &&
|
|
1754
|
+
// Validate password after it changes
|
|
1755
|
+
onChangePassword: (value) => {
|
|
1756
|
+
m.isValid = m.email.includes('@') && value.length >= 6;
|
|
1731
1757
|
},
|
|
1732
1758
|
|
|
1733
|
-
// Sanitize input before setting
|
|
1759
|
+
// Sanitize input before setting — onChanging receives (oldValue, newValue)
|
|
1734
1760
|
onChangingEmail: (oldValue, newValue) => {
|
|
1735
1761
|
return newValue.toLowerCase().trim();
|
|
1736
1762
|
}
|
|
@@ -1854,302 +1880,128 @@ const useEffectDemo = (params: ComponentParams<Struct>) => {
|
|
|
1854
1880
|
|
|
1855
1881
|
In this example the `trackNameChanges` effect accesses `m.firstName` and `m.lastName`, so it re-runs whenever either changes. Clicking **Pause** calls `c.effects.trackNameChanges.pause()`, which suspends the auto-tracking — edits to the name fields no longer update `fullName` until **Resume** is clicked.
|
|
1856
1882
|
|
|
1857
|
-
## Examples (React)
|
|
1883
|
+
## More Examples (React)
|
|
1858
1884
|
|
|
1859
|
-
> **Note:**
|
|
1885
|
+
> **Note:** For basic examples (simple component, parent-child, bindings, events, effects) see the [Getting Started](#getting-started-react) and [Core Concepts](#core-concepts) sections above.
|
|
1860
1886
|
|
|
1861
|
-
###
|
|
1887
|
+
### Service Integration (API Calls)
|
|
1862
1888
|
|
|
1863
|
-
|
|
1864
|
-
// React implementation
|
|
1865
|
-
import { ComponentStruct, ComponentDef, ComponentParams } from '@actdim/dynstruct/componentModel/contracts';
|
|
1866
|
-
import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
|
|
1867
|
-
import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
|
|
1889
|
+
dynstruct integrates with the **service adapter** system from [@actdim/msgmesh](https://github.com/actdim/msgmesh/?tab=readme-ov-file#service-adapters). Adapters automatically register any service class (e.g. an API client) as a message bus provider — channel names, payload types, and return types are all derived from the service class at compile time.
|
|
1868
1890
|
|
|
1869
|
-
|
|
1870
|
-
props: { count: number };
|
|
1871
|
-
actions: { increment: () => void; decrement: () => void };
|
|
1872
|
-
}>;
|
|
1891
|
+
#### 1. Define an API client
|
|
1873
1892
|
|
|
1874
|
-
|
|
1875
|
-
|
|
1876
|
-
props: { count: params.count ?? 0 },
|
|
1877
|
-
actions: {
|
|
1878
|
-
increment: () => { c.model.count++; },
|
|
1879
|
-
decrement: () => { c.model.count--; }
|
|
1880
|
-
},
|
|
1881
|
-
view: (_, c) => (
|
|
1882
|
-
<div>
|
|
1883
|
-
<h2>Counter: {c.model.count}</h2>
|
|
1884
|
-
<button onClick={c.actions.increment}>+</button>
|
|
1885
|
-
<button onClick={c.actions.decrement}>-</button>
|
|
1886
|
-
</div>
|
|
1887
|
-
)
|
|
1888
|
-
};
|
|
1893
|
+
```typescript
|
|
1894
|
+
export type DataItem = { id: number; name: string };
|
|
1889
1895
|
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1896
|
+
export class TestApiClient {
|
|
1897
|
+
static readonly name = 'TestApiClient' as const;
|
|
1898
|
+
readonly name = 'TestApiClient' as const;
|
|
1893
1899
|
|
|
1894
|
-
|
|
1900
|
+
getDataItems(param1: number[], param2: string[]): Promise<DataItem[]> {
|
|
1901
|
+
return fetch('/api/data').then(r => r.json());
|
|
1902
|
+
}
|
|
1903
|
+
}
|
|
1895
1904
|
```
|
|
1896
1905
|
|
|
1897
|
-
|
|
1906
|
+
#### 2. Set up adapters and service provider
|
|
1907
|
+
|
|
1908
|
+
Type utilities from [`@actdim/msgmesh/adapters`](https://www.npmjs.com/package/@actdim/msgmesh) transform the service class into a typed bus structure. Each public method becomes a channel (e.g. `getDataItems` → `API.TEST.GETDATAITEMS`). See [@actdim/msgmesh — Service Adapters](https://github.com/actdim/msgmesh/?tab=readme-ov-file#service-adapters) for details on how the type transformation works.
|
|
1898
1909
|
|
|
1899
1910
|
```typescript
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1911
|
+
import {
|
|
1912
|
+
BaseServiceSuffix, getMsgChannelSelector, MsgProviderAdapter,
|
|
1913
|
+
ToMsgChannelPrefix, ToMsgStruct,
|
|
1914
|
+
} from '@actdim/msgmesh/adapters';
|
|
1915
|
+
import { ServiceProvider } from '@actdim/dynstruct/services/react/ServiceProvider';
|
|
1916
|
+
|
|
1917
|
+
// "TestApiClient" → remove suffix "Client" → uppercase → "API.TEST."
|
|
1918
|
+
type ApiPrefix = 'API';
|
|
1919
|
+
type TestApiChannelPrefix = ToMsgChannelPrefix<
|
|
1920
|
+
typeof TestApiClient.name, ApiPrefix, BaseServiceSuffix
|
|
1921
|
+
>;
|
|
1904
1922
|
|
|
1905
|
-
//
|
|
1906
|
-
type
|
|
1907
|
-
props: { label: string; onClick: () => void };
|
|
1908
|
-
}>;
|
|
1923
|
+
// Transform service methods into a bus struct
|
|
1924
|
+
type ApiMsgStruct = ToMsgStruct<TestApiClient, TestApiChannelPrefix>;
|
|
1909
1925
|
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
label: params.label ?? 'Click',
|
|
1914
|
-
onClick: params.onClick ?? (() => {})
|
|
1915
|
-
},
|
|
1916
|
-
view: (_, c) => (
|
|
1917
|
-
<button onClick={c.model.onClick}>{c.model.label}</button>
|
|
1918
|
-
)
|
|
1919
|
-
};
|
|
1920
|
-
return useComponent(def, params);
|
|
1926
|
+
// Create adapter instances
|
|
1927
|
+
const services: Record<TestApiChannelPrefix, any> = {
|
|
1928
|
+
'API.TEST.': new TestApiClient(),
|
|
1921
1929
|
};
|
|
1922
1930
|
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
};
|
|
1930
|
-
}>;
|
|
1931
|
-
|
|
1932
|
-
const usePanel = (params: ComponentParams<PanelStruct>) => {
|
|
1933
|
-
let c: Component<PanelStruct>;
|
|
1934
|
-
let m: ComponentModel<PanelStruct>;
|
|
1935
|
-
|
|
1936
|
-
const def: ComponentDef<PanelStruct> = {
|
|
1937
|
-
props: {
|
|
1938
|
-
title: params.title ?? 'Panel',
|
|
1939
|
-
clickCount: 0
|
|
1940
|
-
},
|
|
1941
|
-
children: {
|
|
1942
|
-
okButton: useButton({
|
|
1943
|
-
label: 'OK',
|
|
1944
|
-
onClick: () => {
|
|
1945
|
-
m.clickCount++; // Reactive property update
|
|
1946
|
-
console.log('OK clicked');
|
|
1947
|
-
}
|
|
1948
|
-
}),
|
|
1949
|
-
cancelButton: useButton({
|
|
1950
|
-
label: 'Cancel',
|
|
1951
|
-
onClick: () => console.log('Cancel clicked')
|
|
1952
|
-
})
|
|
1953
|
-
},
|
|
1954
|
-
view: (_, c) => (
|
|
1955
|
-
<div className="panel">
|
|
1956
|
-
<h3>{m.title}</h3>
|
|
1957
|
-
<p>Clicks: {m.clickCount}</p>
|
|
1958
|
-
<div className="buttons">
|
|
1959
|
-
<c.children.okButton.View />
|
|
1960
|
-
<c.children.cancelButton.View />
|
|
1961
|
-
</div>
|
|
1962
|
-
</div>
|
|
1963
|
-
)
|
|
1964
|
-
};
|
|
1965
|
-
|
|
1966
|
-
c = useComponent(def, params);
|
|
1967
|
-
m = c.model;
|
|
1968
|
-
return c;
|
|
1969
|
-
};
|
|
1931
|
+
const msgProviderAdapters = Object.entries(services).map(
|
|
1932
|
+
([_, service]) => ({
|
|
1933
|
+
service,
|
|
1934
|
+
channelSelector: getMsgChannelSelector(services),
|
|
1935
|
+
}) as MsgProviderAdapter,
|
|
1936
|
+
);
|
|
1970
1937
|
|
|
1971
|
-
|
|
1938
|
+
// React provider component — wraps children with registered adapters
|
|
1939
|
+
export const ApiServiceProvider = () => ServiceProvider({ adapters: msgProviderAdapters });
|
|
1972
1940
|
```
|
|
1973
1941
|
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
```typescript
|
|
1977
|
-
// React implementation
|
|
1978
|
-
import { ComponentStruct, ComponentDef, ComponentParams, Component, ComponentModel } from '@actdim/dynstruct/componentModel/contracts';
|
|
1979
|
-
import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
|
|
1980
|
-
import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
|
|
1942
|
+
#### 3. Use in a component
|
|
1981
1943
|
|
|
1982
|
-
|
|
1983
|
-
type ProducerStruct = ComponentStruct<AppMsgStruct, {
|
|
1984
|
-
msgScope: {
|
|
1985
|
-
provide: {
|
|
1986
|
-
'EVENT-FIRED': { timestamp: number; data: string };
|
|
1987
|
-
};
|
|
1988
|
-
};
|
|
1989
|
-
}>;
|
|
1944
|
+
Data loading is a plain async function — it has nothing to do with effects. Call it from an event handler (`onReady`) or directly from a button click.
|
|
1990
1945
|
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
provide: {
|
|
1996
|
-
'EVENT-FIRED': {
|
|
1997
|
-
callback: () => ({
|
|
1998
|
-
timestamp: Date.now(),
|
|
1999
|
-
data: 'Event fired from producer'
|
|
2000
|
-
})
|
|
2001
|
-
}
|
|
2002
|
-
}
|
|
2003
|
-
},
|
|
2004
|
-
view: (_, c) => (
|
|
2005
|
-
<button onClick={() => {
|
|
2006
|
-
// Use component's msgBus to send
|
|
2007
|
-
c.msgBus.send({
|
|
2008
|
-
channel: 'EVENT-FIRED',
|
|
2009
|
-
payload: {}
|
|
2010
|
-
});
|
|
2011
|
-
}}>
|
|
2012
|
-
Fire Event
|
|
2013
|
-
</button>
|
|
2014
|
-
)
|
|
1946
|
+
```typescript
|
|
1947
|
+
type Struct = ComponentStruct<ApiMsgStruct, {
|
|
1948
|
+
props: {
|
|
1949
|
+
dataItems: DataItem[];
|
|
2015
1950
|
};
|
|
2016
|
-
return useComponent(def, params);
|
|
2017
|
-
};
|
|
2018
|
-
|
|
2019
|
-
export const Producer = toReact(useProducer);
|
|
2020
|
-
|
|
2021
|
-
// Consumer Component
|
|
2022
|
-
type ConsumerStruct = ComponentStruct<AppMsgStruct, {
|
|
2023
|
-
props: { lastEvent: string };
|
|
2024
1951
|
msgScope: {
|
|
2025
|
-
subscribe:
|
|
2026
|
-
|
|
2027
|
-
};
|
|
1952
|
+
subscribe: ComponentMsgChannels<'API.TEST.GETDATAITEMS'>;
|
|
1953
|
+
publish: ComponentMsgChannels<'API.TEST.GETDATAITEMS'>;
|
|
2028
1954
|
};
|
|
2029
1955
|
}>;
|
|
2030
1956
|
|
|
2031
|
-
const
|
|
2032
|
-
let c: Component<
|
|
2033
|
-
let m: ComponentModel<
|
|
2034
|
-
|
|
2035
|
-
const def: ComponentDef<ConsumerStruct> = {
|
|
2036
|
-
props: { lastEvent: 'No events yet' },
|
|
2037
|
-
// msgBroker subscribes to messages
|
|
2038
|
-
msgBroker: {
|
|
2039
|
-
subscribe: {
|
|
2040
|
-
'EVENT-FIRED': {
|
|
2041
|
-
callback: (msg) => {
|
|
2042
|
-
// Update reactive property
|
|
2043
|
-
m.lastEvent = `${msg.payload.data} at ${new Date(msg.payload.timestamp).toLocaleTimeString()}`;
|
|
2044
|
-
}
|
|
2045
|
-
}
|
|
2046
|
-
}
|
|
2047
|
-
},
|
|
2048
|
-
view: (_, c) => (
|
|
2049
|
-
<div>
|
|
2050
|
-
<p>Last Event: {m.lastEvent}</p>
|
|
2051
|
-
</div>
|
|
2052
|
-
)
|
|
2053
|
-
};
|
|
2054
|
-
|
|
2055
|
-
c = useComponent(def, params);
|
|
2056
|
-
m = c.model; // Properties are now reactive
|
|
2057
|
-
return c;
|
|
2058
|
-
};
|
|
2059
|
-
|
|
2060
|
-
export const Consumer = toReact(useConsumer);
|
|
2061
|
-
```
|
|
2062
|
-
|
|
2063
|
-
### Example 4: Service Integration (API Calls)
|
|
2064
|
-
|
|
2065
|
-
```typescript
|
|
2066
|
-
// React implementation
|
|
2067
|
-
import { ComponentStruct, ComponentDef, ComponentParams, Component, ComponentModel } from '@actdim/dynstruct/componentModel/contracts';
|
|
2068
|
-
import { useComponent, toReact } from '@actdim/dynstruct/componentModel/react';
|
|
2069
|
-
import { AppMsgStruct } from '@actdim/dynstruct/appDomain/appContracts';
|
|
2070
|
-
import { ClientBase } from '@actdim/dynstruct/net/client';
|
|
2071
|
-
import { MsgProviderAdapter } from '@actdim/dynstruct/componentModel/adapters';
|
|
2072
|
-
import { ServiceProvider } from '@actdim/dynstruct/services/ServiceProvider';
|
|
2073
|
-
|
|
2074
|
-
// Define API client
|
|
2075
|
-
class UserApiClient extends ClientBase {
|
|
2076
|
-
constructor() {
|
|
2077
|
-
super({ baseUrl: 'https://api.example.com' });
|
|
2078
|
-
}
|
|
2079
|
-
|
|
2080
|
-
async getUsers() {
|
|
2081
|
-
return this.get<User[]>('/users');
|
|
2082
|
-
}
|
|
1957
|
+
const useApiCallExample = (params: ComponentParams<Struct>) => {
|
|
1958
|
+
let c: Component<Struct>;
|
|
1959
|
+
let m: ComponentModel<Struct>;
|
|
2083
1960
|
|
|
2084
|
-
async
|
|
2085
|
-
|
|
1961
|
+
// Plain async function — not an effect, not an action
|
|
1962
|
+
async function loadData() {
|
|
1963
|
+
const msg = await c.msgBus.request({
|
|
1964
|
+
channel: 'API.TEST.GETDATAITEMS',
|
|
1965
|
+
payloadFn: (fn) => fn([1, 2], ['first', 'second']),
|
|
1966
|
+
});
|
|
1967
|
+
m.dataItems = msg.payload;
|
|
2086
1968
|
}
|
|
2087
1969
|
|
|
2088
|
-
async
|
|
2089
|
-
|
|
1970
|
+
async function clear() {
|
|
1971
|
+
m.dataItems.length = 0;
|
|
2090
1972
|
}
|
|
2091
|
-
}
|
|
2092
|
-
|
|
2093
|
-
// Create adapter
|
|
2094
|
-
const userApiAdapter: MsgProviderAdapter<UserApiClient> = {
|
|
2095
|
-
service: new UserApiClient(),
|
|
2096
|
-
channelSelector: (service, method) => `API.USERS.${method.toUpperCase()}`
|
|
2097
|
-
};
|
|
2098
|
-
|
|
2099
|
-
// Register in app provider
|
|
2100
|
-
export const ApiServiceProvider = () =>
|
|
2101
|
-
ServiceProvider({ adapters: [userApiAdapter] });
|
|
2102
|
-
|
|
2103
|
-
// Use in component
|
|
2104
|
-
type AppStruct = ComponentStruct<AppMsgStruct, {
|
|
2105
|
-
props: { users: User[]; loading: boolean };
|
|
2106
|
-
}>;
|
|
2107
|
-
|
|
2108
|
-
const useApp = (params: ComponentParams<AppStruct>) => {
|
|
2109
|
-
let c: Component<AppStruct>;
|
|
2110
|
-
let m: ComponentModel<AppStruct>;
|
|
2111
1973
|
|
|
2112
|
-
const def: ComponentDef<
|
|
1974
|
+
const def: ComponentDef<Struct> = {
|
|
1975
|
+
regType: 'ApiCallExample',
|
|
2113
1976
|
props: {
|
|
2114
|
-
|
|
2115
|
-
loading: false
|
|
1977
|
+
dataItems: [],
|
|
2116
1978
|
},
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
const response = await component.msgBus.request({
|
|
2121
|
-
channel: 'API.USERS.GETUSERS',
|
|
2122
|
-
payload: {}
|
|
2123
|
-
});
|
|
2124
|
-
m.users = response.payload; // Reactive update
|
|
2125
|
-
m.loading = false;
|
|
2126
|
-
}
|
|
1979
|
+
events: {
|
|
1980
|
+
// Load data when the component is ready
|
|
1981
|
+
onReady: () => { loadData(); },
|
|
2127
1982
|
},
|
|
2128
1983
|
view: (_, c) => (
|
|
2129
|
-
<div>
|
|
2130
|
-
<
|
|
2131
|
-
{
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
))}
|
|
2138
|
-
</ul>
|
|
2139
|
-
)}
|
|
1984
|
+
<div id={c.id}>
|
|
1985
|
+
<button onClick={loadData}>Load data</button>
|
|
1986
|
+
<button onClick={clear}>Clear</button>
|
|
1987
|
+
<ul>
|
|
1988
|
+
{m.dataItems.map((item) => (
|
|
1989
|
+
<li key={item.id}>{item.id}: {item.name}</li>
|
|
1990
|
+
))}
|
|
1991
|
+
</ul>
|
|
2140
1992
|
</div>
|
|
2141
|
-
)
|
|
1993
|
+
),
|
|
2142
1994
|
};
|
|
2143
1995
|
|
|
2144
1996
|
c = useComponent(def, params);
|
|
2145
|
-
m = c.model;
|
|
1997
|
+
m = c.model;
|
|
2146
1998
|
return c;
|
|
2147
1999
|
};
|
|
2148
2000
|
|
|
2149
|
-
export const
|
|
2001
|
+
export const ApiCallExample = toReact(useApiCallExample);
|
|
2150
2002
|
```
|
|
2151
2003
|
|
|
2152
|
-
###
|
|
2004
|
+
### Navigation
|
|
2153
2005
|
|
|
2154
2006
|
```typescript
|
|
2155
2007
|
// React implementation
|
|
@@ -2196,7 +2048,7 @@ const usePage = (params: ComponentParams<PageStruct>) => {
|
|
|
2196
2048
|
export const Page = toReact(usePage);
|
|
2197
2049
|
```
|
|
2198
2050
|
|
|
2199
|
-
###
|
|
2051
|
+
### Authentication & Security
|
|
2200
2052
|
|
|
2201
2053
|
```typescript
|
|
2202
2054
|
// React implementation
|
|
@@ -2321,9 +2173,11 @@ Messages can be filtered by source using `ComponentMsgFilter`:
|
|
|
2321
2173
|
msgBroker: {
|
|
2322
2174
|
subscribe: {
|
|
2323
2175
|
'MY-EVENT': {
|
|
2324
|
-
|
|
2325
|
-
|
|
2326
|
-
|
|
2176
|
+
in: {
|
|
2177
|
+
callback: (msg, component) => {
|
|
2178
|
+
// Only triggered by parent/ancestor components
|
|
2179
|
+
},
|
|
2180
|
+
componentFilter: ComponentMsgFilter.FromAncestors
|
|
2327
2181
|
}
|
|
2328
2182
|
}
|
|
2329
2183
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { BaseAppMsgStruct } from '../appDomain/appContracts';
|
|
2
|
-
import { MsgProviderAdapter } from '
|
|
2
|
+
import { MsgProviderAdapter } from '@actdim/msgmesh/adapters';
|
|
3
3
|
import { MsgBus } from '@actdim/msgmesh/contracts';
|
|
4
4
|
export declare function createServiceProvider(msgBus: MsgBus<BaseAppMsgStruct>, adapters?: MsgProviderAdapter[], abortSignal?: AbortSignal): {
|
|
5
5
|
adapters: MsgProviderAdapter[];
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ServiceProvider.d.ts","sourceRoot":"","sources":["../../src/services/ServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAoB,MAAM,
|
|
1
|
+
{"version":3,"file":"ServiceProvider.d.ts","sourceRoot":"","sources":["../../src/services/ServiceProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAE,MAAM,0BAA0B,CAAC;AAC5D,OAAO,EAAE,kBAAkB,EAAoB,MAAM,0BAA0B,CAAC;AAChF,OAAO,EAAE,MAAM,EAAE,MAAM,2BAA2B,CAAC;AAEnD,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,CAAC,gBAAgB,CAAC,EAAE,QAAQ,CAAC,EAAE,kBAAkB,EAAE,EAAE,WAAW,CAAC,EAAE,WAAW;;;EAUjI"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
import { MsgProviderAdapter } from '@actdim/msgmesh/adapters';
|
|
1
2
|
import { PropsWithChildren, default as React } from 'react';
|
|
2
|
-
import { MsgProviderAdapter } from '../../componentModel/adapters';
|
|
3
3
|
export declare function ServiceProvider(props: PropsWithChildren<{
|
|
4
4
|
adapters?: MsgProviderAdapter[];
|
|
5
5
|
abortSignal?: AbortSignal;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ServiceProvider.d.ts","sourceRoot":"","sources":["../../../src/services/react/ServiceProvider.tsx"],"names":[],"mappings":"AACA,OAAO,
|
|
1
|
+
{"version":3,"file":"ServiceProvider.d.ts","sourceRoot":"","sources":["../../../src/services/react/ServiceProvider.tsx"],"names":[],"mappings":"AACA,OAAO,EAAE,kBAAkB,EAAoB,MAAM,0BAA0B,CAAC;AAChF,OAAO,EAAiB,iBAAiB,EAAmB,MAAM,OAAO,CAAC;AAC1E,OAAO,KAAK,MAAM,OAAO,CAAC;AAO1B,wBAAgB,eAAe,CAC3B,KAAK,EAAE,iBAAiB,CAAC;IACrB,QAAQ,CAAC,EAAE,kBAAkB,EAAE,CAAC;IAChC,WAAW,CAAC,EAAE,WAAW,CAAC;CAC7B,CAAC,qBAyBL"}
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { useComponentContext as a } from "../../componentModel/componentContext.es.js";
|
|
2
|
-
import
|
|
3
|
-
import {
|
|
4
|
-
const s =
|
|
2
|
+
import { registerAdapters as n } from "@actdim/msgmesh/adapters";
|
|
3
|
+
import i, { createContext as c, useLayoutEffect as l } from "react";
|
|
4
|
+
const s = c(void 0);
|
|
5
5
|
function f(t) {
|
|
6
6
|
const r = a(), o = new AbortController();
|
|
7
7
|
let e = t.abortSignal;
|
|
8
|
-
return e && (e = AbortSignal.any([e, o.signal])),
|
|
8
|
+
return e && (e = AbortSignal.any([e, o.signal])), l(() => (n(r.msgBus, t.adapters, e), () => {
|
|
9
9
|
o.abort();
|
|
10
|
-
}), [r.msgBus, t.adapters]), /* @__PURE__ */
|
|
10
|
+
}), [r.msgBus, t.adapters]), /* @__PURE__ */ i.createElement(
|
|
11
11
|
s.Provider,
|
|
12
12
|
{
|
|
13
13
|
value: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@actdim/dynstruct",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.9",
|
|
4
4
|
"description": "A type-safe component system for large-scale apps: explicit dependencies, message bus communication, and structure-first, declarative design",
|
|
5
5
|
"author": "Pavel Borodaev",
|
|
6
6
|
"license": "Proprietary",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
"typecheck": "tsc -p tsconfig.json --noEmit"
|
|
72
72
|
},
|
|
73
73
|
"peerDependencies": {
|
|
74
|
-
"@actdim/msgmesh": "^1.2.
|
|
74
|
+
"@actdim/msgmesh": "^1.2.9",
|
|
75
75
|
"@actdim/utico": "^1.1.2",
|
|
76
76
|
"http-status": "^2.1.0",
|
|
77
77
|
"jwt-decode": "^4.0.0",
|
|
@@ -1,20 +0,0 @@
|
|
|
1
|
-
import { MsgBus, MsgStructFactory } from '@actdim/msgmesh/contracts';
|
|
2
|
-
import { AddPrefix, Filter, Func, RemoveSuffix, Skip, ToUpper } from '@actdim/utico/typeCore';
|
|
3
|
-
export type MsgProviderAdapter = {
|
|
4
|
-
service: any;
|
|
5
|
-
channelSelector: (service: any, methodName: string) => string;
|
|
6
|
-
};
|
|
7
|
-
export declare function registerAdapters(msgBus: MsgBus<any>, adapters: MsgProviderAdapter[], abortSignal?: AbortSignal): void;
|
|
8
|
-
export type BaseServiceSuffix = 'CLIENT' | 'API' | 'SERVICE' | 'FETCHER' | 'CONTROLLER' | 'LOADER' | 'REPOSITORY' | 'PROVIDER';
|
|
9
|
-
export type BaseWordSeparator = ".";
|
|
10
|
-
export type ToMsgChannelPrefix<TServiceName extends string, Prefix extends string, Suffix extends string = BaseServiceSuffix, WordSeparator extends string = BaseWordSeparator> = `${Prefix}${WordSeparator}${RemoveSuffix<Uppercase<TServiceName>, Suffix>}${WordSeparator}`;
|
|
11
|
-
type ToMsgStructSource<TService, TPrefix extends string, TSkip extends keyof TService = never> = Filter<ToUpper<AddPrefix<Skip<TService, TSkip>, TPrefix>>, Func>;
|
|
12
|
-
export type ToMsgStruct<TService, TPrefix extends string, TSkip extends keyof TService = never, TMsgStructSource = ToMsgStructSource<TService, TPrefix, TSkip>> = MsgStructFactory<{
|
|
13
|
-
[K in keyof TMsgStructSource as TMsgStructSource[K] extends Func ? (Uppercase<K extends string ? K : never>) : never]: {
|
|
14
|
-
in: TMsgStructSource[K] extends Func ? Parameters<TMsgStructSource[K]> : never;
|
|
15
|
-
out: TMsgStructSource[K] extends Func ? ReturnType<TMsgStructSource[K]> : never;
|
|
16
|
-
};
|
|
17
|
-
}>;
|
|
18
|
-
export declare function getMsgChannelSelector<TTPrefix extends string>(services: Record<TTPrefix, any>): (service: any, methodName: string) => string;
|
|
19
|
-
export {};
|
|
20
|
-
//# sourceMappingURL=adapters.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adapters.d.ts","sourceRoot":"","sources":["../../src/componentModel/adapters.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,2BAA2B,CAAC;AACrE,OAAO,EAAE,SAAS,EAAE,MAAM,EAAE,IAAI,EAAE,YAAY,EAAE,IAAI,EAAE,OAAO,EAAE,MAAM,wBAAwB,CAAC;AAY9F,MAAM,MAAM,kBAAkB,GAAG;IAC7B,OAAO,EAAE,GAAG,CAAC;IAEb,eAAe,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;CACjE,CAAC;AAEF,wBAAgB,gBAAgB,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,CAAC,EAAE,QAAQ,EAAE,kBAAkB,EAAE,EAAE,WAAW,CAAC,EAAE,WAAW,QAwB9G;AAED,MAAM,MAAM,iBAAiB,GAAG,QAAQ,GAAG,KAAK,GAAG,SAAS,GAAG,SAAS,GAAG,YAAY,GAAG,QAAQ,GAAG,YAAY,GAAG,UAAU,CAAC;AAC/H,MAAM,MAAM,iBAAiB,GAAG,GAAG,CAAC;AAIpC,MAAM,MAAM,kBAAkB,CAC1B,YAAY,SAAS,MAAM,EAC3B,MAAM,SAAS,MAAM,EACrB,MAAM,SAAS,MAAM,GAAG,iBAAiB,EACzC,aAAa,SAAS,MAAM,GAAG,iBAAiB,IAChD,GAAG,MAAM,GAAG,aAAa,GAAG,YAAY,CAAC,SAAS,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC,GAAG,aAAa,EAAE,CAAC;AAEhG,KAAK,iBAAiB,CAAC,QAAQ,EAAE,OAAO,SAAS,MAAM,EAAE,KAAK,SAAS,MAAM,QAAQ,GAAG,KAAK,IAAI,MAAM,CACnG,OAAO,CAAC,SAAS,CAAC,IAAI,CAAC,QAAQ,EAAE,KAAK,CAAC,EAAE,OAAO,CAAC,CAAC,EAClD,IAAI,CACP,CAAC;AAEF,MAAM,MAAM,WAAW,CAAC,QAAQ,EAAE,OAAO,SAAS,MAAM,EAAE,KAAK,SAAS,MAAM,QAAQ,GAAG,KAAK,EAAE,gBAAgB,GAAG,iBAAiB,CAAC,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,IAAI,gBAAgB,CAAC;KAC9K,CAAC,IAAI,MAAM,gBAAgB,IAAI,gBAAgB,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,SAAS,MAAM,GAAG,CAAC,GAAG,KAAK,CAAC,CAAC,GAAG,KAAK,GAAG;QACnH,EAAE,EAAE,gBAAgB,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;QAC/E,GAAG,EAAE,gBAAgB,CAAC,CAAC,CAAC,SAAS,IAAI,GAAG,UAAU,CAAC,gBAAgB,CAAC,CAAC,CAAC,CAAC,GAAG,KAAK,CAAC;KACnF;CACJ,CAAC,CAAC;AAEH,wBAAgB,qBAAqB,CAAC,QAAQ,SAAS,MAAM,EACzD,QAAQ,EAAE,MAAM,CAAC,QAAQ,EAAE,GAAG,CAAC,IAEvB,SAAS,GAAG,EAAE,YAAY,MAAM,YAO3C"}
|
|
@@ -1,33 +0,0 @@
|
|
|
1
|
-
const s = (t) => Object.getOwnPropertyNames(t).filter(
|
|
2
|
-
(e) => e !== "constructor" && typeof t[e] == "function"
|
|
3
|
-
);
|
|
4
|
-
function l(t, e, n) {
|
|
5
|
-
if (e)
|
|
6
|
-
for (const o of e) {
|
|
7
|
-
const { service: r, channelSelector: c } = o;
|
|
8
|
-
if (!r || !c)
|
|
9
|
-
throw new Error("Service and channelSelector are required for an adapter");
|
|
10
|
-
for (const a of s(Object.getPrototypeOf(r))) {
|
|
11
|
-
const f = c?.(r, a);
|
|
12
|
-
f && t.provide({
|
|
13
|
-
channel: f,
|
|
14
|
-
topic: "/.*/",
|
|
15
|
-
callback: (i) => r[a](...i.payload || []),
|
|
16
|
-
options: {
|
|
17
|
-
abortSignal: n
|
|
18
|
-
}
|
|
19
|
-
});
|
|
20
|
-
}
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
function p(t) {
|
|
24
|
-
return (e, n) => {
|
|
25
|
-
const o = Object.entries(t).find((r) => r[1] === e);
|
|
26
|
-
return o ? `${o[0]}${n.toUpperCase()}` : null;
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
export {
|
|
30
|
-
p as getMsgChannelSelector,
|
|
31
|
-
l as registerAdapters
|
|
32
|
-
};
|
|
33
|
-
//# sourceMappingURL=adapters.es.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"adapters.es.js","sources":["../../src/componentModel/adapters.ts"],"sourcesContent":null,"names":["getMethodNames","client","name","registerAdapters","msgBus","adapters","abortSignal","adapter","service","channelSelector","methodName","channel","msg","getMsgChannelSelector","services","entry"],"mappings":"AAIA,MAAMA,IAAiB,CAACC,MAEb,OAAO,oBAAoBA,CAAM,EAAE;AAAA,EACtC,CAACC,MAASA,MAAS,iBAAiB,OAAOD,EAAOC,CAAI,KAAM;AAAA;AAa7D,SAASC,EAAiBC,GAAqBC,GAAgCC,GAA2B;AAC7G,MAAID;AACA,eAAWE,KAAWF,GAAU;AAC5B,YAAM,EAAE,SAAAG,GAAS,iBAAAC,EAAA,IAAoBF;AACrC,UAAI,CAACC,KAAW,CAACC;AACb,cAAM,IAAI,MAAM,yDAAyD;AAE7E,iBAAWC,KAAcV,EAAe,OAAO,eAAeQ,CAAO,CAAC,GAAG;AACrE,cAAMG,IAAUF,IAAkBD,GAASE,CAAU;AACrD,QAAIC,KACAP,EAAO,QAAQ;AAAA,UACX,SAAAO;AAAA,UACA,OAAO;AAAA,UACP,UAAU,CAACC,MACCJ,EAAQE,CAAU,EAAW,GAAKE,EAAI,WAAW,CAAA,CAAa;AAAA,UAE1E,SAAS;AAAA,YACL,aAAAN;AAAA,UAAA;AAAA,QACJ,CACH;AAAA,MAET;AAAA,IACJ;AAER;AA0BO,SAASO,EACZC,GACF;AACE,SAAO,CAACN,GAAcE,MAAuB;AACzC,UAAMK,IAAQ,OAAO,QAAQD,CAAQ,EAAE,KAAK,CAACC,MAAUA,EAAM,CAAC,MAAMP,CAAO;AAC3E,WAAKO,IAGE,GAAGA,EAAM,CAAC,CAAC,GAAGL,EAAW,aAAa,KAFlC;AAAA,EAGf;AACJ;"}
|