lanka 1.0.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/LICENSE +21 -0
- package/README.md +242 -0
- package/dist/ALankaGateway-ExlRGT3D.d.ts +122 -0
- package/dist/ALankaSharedStore-B7uepuuk.d.ts +29 -0
- package/dist/ILankaApiError-zI_2bNs0.d.ts +16 -0
- package/dist/ILankaRuntimeConfig-Vl436GWK.d.ts +62 -0
- package/dist/ILankaScenarioMetadata-Bu-yggTZ.d.ts +14 -0
- package/dist/ILankaScenarioVM-DuCyPoyT.d.ts +260 -0
- package/dist/LankaError-B1HtuIkw.d.ts +86 -0
- package/dist/LankaScenarioLocator-BGQHwf3n.d.ts +144 -0
- package/dist/LankaSharedStoreLocator-MvCpav5F.d.ts +126 -0
- package/dist/TLankaErrorHandler-Yfqtdh1M.d.ts +3 -0
- package/dist/_extend/index.d.ts +34 -0
- package/dist/_extend/index.js +36 -0
- package/dist/_extend/index.js.map +1 -0
- package/dist/_internal/index.d.ts +43 -0
- package/dist/_internal/index.js +34 -0
- package/dist/_internal/index.js.map +1 -0
- package/dist/activeRuntime-FcsSJvUg.d.ts +84 -0
- package/dist/bootstrap/index.d.ts +242 -0
- package/dist/bootstrap/index.js +22 -0
- package/dist/bootstrap/index.js.map +1 -0
- package/dist/chunk-73IVH3C6.js +52 -0
- package/dist/chunk-73IVH3C6.js.map +1 -0
- package/dist/chunk-BGVDPDX4.js +29 -0
- package/dist/chunk-BGVDPDX4.js.map +1 -0
- package/dist/chunk-C2HP7CRD.js +224 -0
- package/dist/chunk-C2HP7CRD.js.map +1 -0
- package/dist/chunk-D27MREPB.js +13 -0
- package/dist/chunk-D27MREPB.js.map +1 -0
- package/dist/chunk-DTO27QFR.js +59 -0
- package/dist/chunk-DTO27QFR.js.map +1 -0
- package/dist/chunk-EWVDJYCC.js +296 -0
- package/dist/chunk-EWVDJYCC.js.map +1 -0
- package/dist/chunk-FIR4XTBL.js +136 -0
- package/dist/chunk-FIR4XTBL.js.map +1 -0
- package/dist/chunk-ILQNYQY5.js +200 -0
- package/dist/chunk-ILQNYQY5.js.map +1 -0
- package/dist/chunk-J5LA3Q6D.js +14 -0
- package/dist/chunk-J5LA3Q6D.js.map +1 -0
- package/dist/chunk-MDZ42XRH.js +10 -0
- package/dist/chunk-MDZ42XRH.js.map +1 -0
- package/dist/chunk-Q3SOVBIJ.js +176 -0
- package/dist/chunk-Q3SOVBIJ.js.map +1 -0
- package/dist/chunk-Q7QESSYF.js +49 -0
- package/dist/chunk-Q7QESSYF.js.map +1 -0
- package/dist/chunk-RKYKK6MN.js +19 -0
- package/dist/chunk-RKYKK6MN.js.map +1 -0
- package/dist/chunk-RYFZCAQ3.js +637 -0
- package/dist/chunk-RYFZCAQ3.js.map +1 -0
- package/dist/chunk-UJEC7H6K.js +39 -0
- package/dist/chunk-UJEC7H6K.js.map +1 -0
- package/dist/chunk-XESL274R.js +23 -0
- package/dist/chunk-XESL274R.js.map +1 -0
- package/dist/chunk-YR4MZXMU.js +13 -0
- package/dist/chunk-YR4MZXMU.js.map +1 -0
- package/dist/config/index.d.ts +63 -0
- package/dist/config/index.js +16 -0
- package/dist/config/index.js.map +1 -0
- package/dist/createLankaScope-Bc_vChRs.d.ts +30 -0
- package/dist/errors/index.d.ts +37 -0
- package/dist/errors/index.js +16 -0
- package/dist/errors/index.js.map +1 -0
- package/dist/gateway/index.d.ts +258 -0
- package/dist/gateway/index.js +438 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/lankaHttpInFlight-Bk1eIuSx.d.ts +43 -0
- package/dist/lankaRequestMiddleware-DAC5kCb7.d.ts +62 -0
- package/dist/lankaStandardValidator-CL-r-zEV.d.ts +34 -0
- package/dist/locator/index.d.ts +282 -0
- package/dist/locator/index.js +67 -0
- package/dist/locator/index.js.map +1 -0
- package/dist/logger/index.d.ts +146 -0
- package/dist/logger/index.js +11 -0
- package/dist/logger/index.js.map +1 -0
- package/dist/mock/index.d.ts +16 -0
- package/dist/mock/index.js +58 -0
- package/dist/mock/index.js.map +1 -0
- package/dist/role/index.d.ts +48 -0
- package/dist/role/index.js +7 -0
- package/dist/role/index.js.map +1 -0
- package/dist/scenario/index.d.ts +204 -0
- package/dist/scenario/index.js +28 -0
- package/dist/scenario/index.js.map +1 -0
- package/dist/validation/index.d.ts +18 -0
- package/dist/validation/index.js +53 -0
- package/dist/validation/index.js.map +1 -0
- package/dist/viewmodel/index.d.ts +478 -0
- package/dist/viewmodel/index.js +701 -0
- package/dist/viewmodel/index.js.map +1 -0
- package/package.json +99 -0
- package/skills/lanka-core/SKILL.md +241 -0
- package/skills/lanka-core/reference.md +1090 -0
- package/skills/lanka-packages/SKILL.md +70 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 lankajs contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
# lanka
|
|
2
|
+
|
|
3
|
+
**◆ core** · Core
|
|
4
|
+
|
|
5
|
+
> Ten subsystems, two peer dependencies, five extension points.
|
|
6
|
+
|
|
7
|
+
One, unscoped. Everything depends on it; it depends on nothing.
|
|
8
|
+
|
|
9
|
+
**Runs in:** the browser, node and React Native — everywhere.
|
|
10
|
+
|
|
11
|
+
**How to use it:** [GUIDE.md](./GUIDE.md) — the user guide, with examples. **How to change it:** [SKILL.md](./SKILL.md).
|
|
12
|
+
|
|
13
|
+
## Extension points core declares
|
|
14
|
+
|
|
15
|
+
| Point | What it gives | Available |
|
|
16
|
+
| --- | --- | --- |
|
|
17
|
+
| `useRequestMiddleware(mw)` | wrap every request | yes |
|
|
18
|
+
| `inFlight.subscribe(fn)` | observe the number of requests on the wire | yes |
|
|
19
|
+
| `lankaEventBus.addMiddleware` | intercept bus events | yes |
|
|
20
|
+
| `LankaLogger sinks` | where log output goes | yes |
|
|
21
|
+
| `use(plugin)` | register a whole plugin | yes |
|
|
22
|
+
|
|
23
|
+
The list is closed on purpose. Each point is a public contract for the lifetime of a
|
|
24
|
+
major version, so anything that can be a module must be a module.
|
|
25
|
+
|
|
26
|
+
## Subsystems
|
|
27
|
+
|
|
28
|
+
Flat, with no intermediate `Layers/`: **folder = subpath in `exports` = line in this list.**
|
|
29
|
+
|
|
30
|
+
Each subsystem is exported through its BARREL (`index.ts`), so public is exactly what the
|
|
31
|
+
barrel lists, moving a file inside a subsystem is not a breaking change, and a consumer's
|
|
32
|
+
import reads `lanka/gateway`.
|
|
33
|
+
|
|
34
|
+
`internal/` is not in the map — that is what may be refactored without a major. Hence the
|
|
35
|
+
rule: a primitive a neighbouring package needs has two honest exits, becoming public here
|
|
36
|
+
or moving to the neighbour. `src/publicSurface.test.ts` checks the map, the barrels and
|
|
37
|
+
the seal on `internal`, and catches a subsystem added as a folder and forgotten in
|
|
38
|
+
`exports`.
|
|
39
|
+
|
|
40
|
+
- `src/bootstrap/`
|
|
41
|
+
- `src/role/`
|
|
42
|
+
- `src/config/`
|
|
43
|
+
- `src/locator/`
|
|
44
|
+
- `src/gateway/`
|
|
45
|
+
- `src/validation/`
|
|
46
|
+
- `src/mock/`
|
|
47
|
+
- `src/errors/`
|
|
48
|
+
- `src/scenario/`
|
|
49
|
+
- `src/viewmodel/`
|
|
50
|
+
- `src/logger/`
|
|
51
|
+
- `src/internal/` — **not exported.** Refactored without a major.
|
|
52
|
+
|
|
53
|
+
## Instance
|
|
54
|
+
|
|
55
|
+
```ts
|
|
56
|
+
const lanka = createLanka({ host, flags });
|
|
57
|
+
await lanka.bootstrap({ services });
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The instance owns all framework state: the bus, the scenario registries, four locator
|
|
61
|
+
caches, the config and the in-flight request counter. Module-level state made three
|
|
62
|
+
things impossible, none of which looked like a bug: two apps in one process shared a
|
|
63
|
+
bus, SSR reused state between different users' requests, and test isolation rested on a
|
|
64
|
+
global `beforeEach` reaching into internal registries.
|
|
65
|
+
|
|
66
|
+
**Ambient facades** — `lankaEventBus.dispatch`, `lankaSingletons.foo`, `getLankaFlags()`,
|
|
67
|
+
`lankaHttpInFlight` — resolve THE ONE active instance (`internal/activeRuntime.ts`).
|
|
68
|
+
They exist for callers that cannot hold an instance: a user-extended `ALankaScenario`,
|
|
69
|
+
the static `LankaScenarioBootstrap`, a module package with no instance in scope.
|
|
70
|
+
Isolation belongs to the instance holder; a facade cannot offer it.
|
|
71
|
+
|
|
72
|
+
One thing stays at module level deliberately: `ALankaScenario` collects constructed
|
|
73
|
+
scenarios into a static pool. That is not runtime state but a REGISTRY OF DEFINITIONS —
|
|
74
|
+
the classes come from one `@lanka_di/Scenarios` barrel and both instances must see the
|
|
75
|
+
same list. Splitting it would be divergence, not isolation.
|
|
76
|
+
|
|
77
|
+
## Failure
|
|
78
|
+
|
|
79
|
+
`LankaError` carries a TAGGED kind: `network` · `timeout` · `aborted` · `http` · `schema` ·
|
|
80
|
+
`domain`. Six rather than one, because each demands something different of the interface:
|
|
81
|
+
a network failure invites a retry, an aborted request is not shown at all (the user left),
|
|
82
|
+
a schema break is reported as a break rather than as the user's fault.
|
|
83
|
+
|
|
84
|
+
The kind is assigned where the failure is born: in `ALankaRequest.execute`, the single
|
|
85
|
+
point every request passes through.
|
|
86
|
+
|
|
87
|
+
Parsing a particular response body format is NOT here: which JSON the backend sends is
|
|
88
|
+
policy, and policy lives in `@lankajs/plugin-http`.
|
|
89
|
+
|
|
90
|
+
## Host contract
|
|
91
|
+
|
|
92
|
+
```ts
|
|
93
|
+
interface ILankaHost {
|
|
94
|
+
apiBaseUrl: string;
|
|
95
|
+
httpErrorMessage(status: number): string;
|
|
96
|
+
networkErrorMessage(): string;
|
|
97
|
+
timeoutErrorMessage(): string;
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
All required, and that is the choice: a forgotten field is a compile error in the one
|
|
102
|
+
place it can be passed, rather than a wrong-language string in the interface or an
|
|
103
|
+
`undefined` inside a request URL. `ALankaGateway.endpoint()` prefixes with `apiBaseUrl`.
|
|
104
|
+
|
|
105
|
+
### Request middleware shape
|
|
106
|
+
|
|
107
|
+
**Request middleware is a WRAPPER, not a set of hooks.** The `(ctx, next) => …` shape,
|
|
108
|
+
because retry cannot be expressed with `onRequest`/`onResponse`/`onError`: `onError` can
|
|
109
|
+
replace an error but cannot run the request again — and retry plus auth refresh are the
|
|
110
|
+
two main abilities of a request-policy plugin. Registered first wraps the rest.
|
|
111
|
+
|
|
112
|
+
The objection to `next()` that holds for the event bus does not carry over: there a
|
|
113
|
+
middleware that skips `next` SILENCES the event, here it returns a value instead of a
|
|
114
|
+
request, and the caller sees it.
|
|
115
|
+
|
|
116
|
+
**Cancellation and timeout.** `execute` takes `signal` and `timeoutMs`; the default
|
|
117
|
+
timeout is per instance. The framework combines the caller's signal and its own timer
|
|
118
|
+
into ONE signal and still distinguishes who aborted: a timed-out request yields `timeout`
|
|
119
|
+
and is shown, a caller-aborted one yields `aborted` and stays silent. `AbortSignal` does
|
|
120
|
+
not distinguish them — it has one `abort` for everyone.
|
|
121
|
+
|
|
122
|
+
## Response body validation
|
|
123
|
+
|
|
124
|
+
The port accepts ANY schema implementing Standard Schema: zod 4, valibot, arktype. An
|
|
125
|
+
abstraction typed by its single implementation is not an abstraction; the proof is a
|
|
126
|
+
second implementation passing the same assertions, and it is in the tests.
|
|
127
|
+
|
|
128
|
+
There are no adapter classes: a schema describes itself. The app chooses the library —
|
|
129
|
+
`@lankajs/zod`, `@lankajs/valibot`, or neither, working with schemas directly.
|
|
130
|
+
|
|
131
|
+
An async schema is rejected LOUDLY. Standard Schema allows `validate` to return a
|
|
132
|
+
promise, a synchronous port cannot await it, and answering "fine" would let unvalidated
|
|
133
|
+
data through — a check that cannot fail reporting success.
|
|
134
|
+
|
|
135
|
+
## Lifetime
|
|
136
|
+
|
|
137
|
+
`lanka.createScope()` gives a lifetime shorter than the application's: an object created
|
|
138
|
+
in a scope goes away with it.
|
|
139
|
+
|
|
140
|
+
A scope takes only ITS OWN objects. One that took others' would be more dangerous than no
|
|
141
|
+
scopes at all: closing a screen would break the app. Resolving from a closed scope is
|
|
142
|
+
rejected loudly — it is almost always a reference that outlived its screen.
|
|
143
|
+
|
|
144
|
+
A lazy ViewModel has `dispose()` for the same reason: it subscribes to scenarios on first
|
|
145
|
+
use, and without explicit disposal the subscription outlives the screen that created it.
|
|
146
|
+
|
|
147
|
+
## Access-tracking blind spot
|
|
148
|
+
|
|
149
|
+
A consumer re-renders only for keys it READ through the proxy. An action computing a
|
|
150
|
+
value via `get()` reads state past the proxy — so a component whose only link to a key
|
|
151
|
+
goes through such a getter never re-renders: the screen freezes and there is no error.
|
|
152
|
+
|
|
153
|
+
It cannot be fixed in the view: destructuring "for the side effect" reads as dead code and
|
|
154
|
+
the next refactor or lint autofix removes it. The fix is
|
|
155
|
+
`enableAccessTrackingOptimization: false`, and so that nobody has to remember it, in
|
|
156
|
+
development the mismatch announces itself: the framework sees that a key changed, that no
|
|
157
|
+
re-render will follow, and that the component reads that key through a getter — and warns
|
|
158
|
+
with the ViewModel and key names.
|
|
159
|
+
|
|
160
|
+
The trap stays silent on healthy code: not reading what you do not need is the work
|
|
161
|
+
tracking exists for.
|
|
162
|
+
|
|
163
|
+
## Event bus
|
|
164
|
+
|
|
165
|
+
**Buffering on request only.** Buffer depth is the maximum of what the event declares
|
|
166
|
+
(`registerEvent({ replay: N })`) and what subscribers ask for; when the last asker
|
|
167
|
+
unsubscribes the buffer is cleared. Buffering every payload unconditionally holds
|
|
168
|
+
personal data in memory with no consumer at all.
|
|
169
|
+
|
|
170
|
+
**`replay` counts values:** `false | "last" | N`, where `true` means "last".
|
|
171
|
+
|
|
172
|
+
**`subscribe` returns an unsubscribe.** It removes EXACTLY that subscription: by callback,
|
|
173
|
+
two subscriptions of one function are indistinguishable.
|
|
174
|
+
|
|
175
|
+
**Delivery iterates a COPY of the subscriber list.** A subscriber may unsubscribe inside
|
|
176
|
+
its own handler, and unsubscribing splices the same array, so iterating the original
|
|
177
|
+
skipped a neighbour silently. The deliberate side effect: subscribing during delivery
|
|
178
|
+
waits for the next event.
|
|
179
|
+
|
|
180
|
+
**`registerEvent` merges metadata** instead of recreating state; recreating dropped every
|
|
181
|
+
subscriber silently.
|
|
182
|
+
|
|
183
|
+
**Middleware returns a decision** — `"pass"` or `{ stop: reason }`. A `next()`-based shape
|
|
184
|
+
means not calling it makes the event vanish SILENTLY: a mechanism that exists for
|
|
185
|
+
observability would be its own blind spot. A stop is written to the event log and is
|
|
186
|
+
visible to the inspector. An exception in middleware is treated the same way.
|
|
187
|
+
|
|
188
|
+
## Which of the three carries a connection
|
|
189
|
+
|
|
190
|
+
The framework ships a ViewModel, a scenario and a shared store, and the hardest
|
|
191
|
+
question is not how any of them works but which one a connection belongs to. The
|
|
192
|
+
ladder, in order — take the first rung that fits and stop:
|
|
193
|
+
|
|
194
|
+
1. **One ViewModel owns the state.** Start here always. A screen with a question
|
|
195
|
+
about its own data has no connection to carry.
|
|
196
|
+
2. **Another must REACT — a scenario.** It carries a fact (`GapCompleted`), not a
|
|
197
|
+
command, and each side decides what the fact means for its own state. Nobody
|
|
198
|
+
imports anybody: that is the whole reason the layer exists.
|
|
199
|
+
3. **Several must CO-EDIT one state — one shared store.** Only when scenarios have
|
|
200
|
+
turned into synchronisation: the same value written in three ViewModels, kept in
|
|
201
|
+
step by hand, racing on a slow answer. Keep the store scoped to the feature and
|
|
202
|
+
give it explicit reset points.
|
|
203
|
+
4. **The store became a bus — the feature boundary is wrong.** A shared store that
|
|
204
|
+
everything reads is global state with extra steps. Split the feature and hand
|
|
205
|
+
cross-feature connections back to scenarios.
|
|
206
|
+
|
|
207
|
+
The rungs are not styles to pick between. Each costs more than the one above: a
|
|
208
|
+
scenario is a name in two files, a shared store is a lifetime somebody has to own.
|
|
209
|
+
|
|
210
|
+
`no-viewmodel-to-viewmodel` in `@lankajs/tool-eslint` enforces the only hard part —
|
|
211
|
+
that rung 1 never reaches sideways — and the core playground runs rungs 2 and 3
|
|
212
|
+
beside each other: a scenario writes into a ViewModel that never heard of the
|
|
213
|
+
trigger, and two ViewModels share one selection through `PlaygroundTodoStore`.
|
|
214
|
+
|
|
215
|
+
## Naming rule
|
|
216
|
+
|
|
217
|
+
**Anything the consumer writes in their own code carries the framework name.**
|
|
218
|
+
`ALankaGateway`, `ALankaScenario`, `createLankaVM`, `lankaEventBus`, `LankaLogger`,
|
|
219
|
+
`lankaSingletons`.
|
|
220
|
+
|
|
221
|
+
Two reasons, the second the stronger one. A reader of an unfamiliar file can see where
|
|
222
|
+
`ALankaGateway` came from; `AGateway` says nothing. And the framework does not squat
|
|
223
|
+
popular names — `Logger`, `EventBus`, `Storage`, `Singletons`, `Scenarios` are exactly
|
|
224
|
+
the names an app wants for ITS own things, and a library that takes them forces the app
|
|
225
|
+
to rename.
|
|
226
|
+
|
|
227
|
+
`ViewModel` shortens to `VM` in class, factory and type names: `ALankaVM` and
|
|
228
|
+
`createLankaVM` are written in every screen file — the same ViewModel reached from
|
|
229
|
+
either style, one of the nine roles `skills/parity/SKILL.md` governs.
|
|
230
|
+
|
|
231
|
+
Three names stay bare — `isRecord`, `getStringField`, `generateUuid`. They are pure
|
|
232
|
+
utilities over ordinary values; a collision there is resolved at the import site, which
|
|
233
|
+
is not available for a class that gets EXTENDED.
|
|
234
|
+
|
|
235
|
+
The rule is executed by `core/src/brand.test.ts`, not stated in a document.
|
|
236
|
+
|
|
237
|
+
`.lanka_di` barrel names (`Gateways.ts`, `Scenarios.ts`, …) are NOT branded: those files
|
|
238
|
+
belong to the consumer and the folder already says whose they are.
|
|
239
|
+
|
|
240
|
+
---
|
|
241
|
+
|
|
242
|
+
Repository map: [../README.md](../README.md)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { I as ILankaValidator } from './lankaStandardValidator-CL-r-zEV.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Transport options plus what core controls.
|
|
5
|
+
*
|
|
6
|
+
* `signal` and `timeoutMs` are taken out and never reach the transport as given:
|
|
7
|
+
* core combines them into one signal and passes that.
|
|
8
|
+
*/
|
|
9
|
+
type TLankaExecuteOptions<TOptions> = TOptions & {
|
|
10
|
+
signal?: AbortSignal;
|
|
11
|
+
timeoutMs?: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* What a gateway needs of a request: one method that sends and returns a value.
|
|
16
|
+
*
|
|
17
|
+
* The port, so a consumer can supply their OWN request rather than inheriting
|
|
18
|
+
* from ours. `ALankaRequest` implements it and remains the convenient way in —
|
|
19
|
+
* it brings middleware composition, deadline resolution, in-flight accounting
|
|
20
|
+
* and failure classification — but a consumer with a different transport story
|
|
21
|
+
* (a native bridge, an offline queue, a test double that never touches the
|
|
22
|
+
* network) needs none of that and should not have to extend a class to be
|
|
23
|
+
* accepted.
|
|
24
|
+
*
|
|
25
|
+
* `mockHandler` is part of the contract rather than an implementation detail: it
|
|
26
|
+
* is how development without a backend works, and a request that ignored it
|
|
27
|
+
* would silently disable mock mode for the gateway holding it.
|
|
28
|
+
*/
|
|
29
|
+
interface ILankaRequest<TOptions = RequestInit> {
|
|
30
|
+
/**
|
|
31
|
+
* Sends the request and returns the value the caller asked for.
|
|
32
|
+
*
|
|
33
|
+
* Rejects with a `LankaError` carrying a kind — `http`, `network`, `timeout`,
|
|
34
|
+
* `aborted`, `schema` — because the kind is what a caller branches on: a
|
|
35
|
+
* timeout is shown, a cancellation is not, and only a network failure invites
|
|
36
|
+
* a retry.
|
|
37
|
+
*/
|
|
38
|
+
execute<TReturn = Response>(endpoint: string, options?: TLankaExecuteOptions<TOptions>, mockHandler?: () => Promise<TReturn>): Promise<TReturn>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
type TLankaQueryParams = string | number | boolean | null | undefined | TLankaQueryParams[] | {
|
|
42
|
+
[key: string]: TLankaQueryParams;
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
type TLankaQueryBuilder = (params: Record<string, TLankaQueryParams>) => URLSearchParams;
|
|
46
|
+
|
|
47
|
+
interface IALankaGatewayConfig<TOptions> {
|
|
48
|
+
/**
|
|
49
|
+
* The request this gateway sends through. Defaults to `LankaFetchJsonRequest`.
|
|
50
|
+
*
|
|
51
|
+
* Each request kind has its own logic — one answers a parsed JSON body, another
|
|
52
|
+
* the raw `Response`, another uploads multipart — and JSON over `fetch` is what
|
|
53
|
+
* almost every gateway wants. So it is supplied when the gateway is NOT
|
|
54
|
+
* ordinary, and omitted otherwise.
|
|
55
|
+
*
|
|
56
|
+
* Typed as the PORT: `ALankaRequest` is the convenient implementation, not a
|
|
57
|
+
* requirement. A consumer with their own transport story supplies their own,
|
|
58
|
+
* and a test supplies one that never leaves the process.
|
|
59
|
+
*/
|
|
60
|
+
request?: ILankaRequest<TOptions>;
|
|
61
|
+
/** Base API path for this gateway, e.g. "/admin/company" */
|
|
62
|
+
basePath?: string;
|
|
63
|
+
/**
|
|
64
|
+
* Serialises query parameters for every request of this gateway.
|
|
65
|
+
*
|
|
66
|
+
* An error-body handler does NOT belong here: it is given to the request —
|
|
67
|
+
* `new LankaFetchJsonRequest({ errorHandler })` — which is the only layer that
|
|
68
|
+
* reads it.
|
|
69
|
+
*/
|
|
70
|
+
queryParamsHandler?: TLankaQueryBuilder;
|
|
71
|
+
/** Validation service (DI). May be replaced for tests / migration. */
|
|
72
|
+
validationService?: ILankaValidator;
|
|
73
|
+
/**
|
|
74
|
+
* Enable mock mode. If undefined, reads from infrastructure flags (isMockMode).
|
|
75
|
+
* If true, mock handlers passed to request methods will be used instead of real requests.
|
|
76
|
+
*/
|
|
77
|
+
useMock?: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
declare abstract class ALankaGateway<TOptions = RequestInit> {
|
|
81
|
+
protected requestExecutor: ILankaRequest<TOptions>;
|
|
82
|
+
protected queryParamsHandler: TLankaQueryBuilder;
|
|
83
|
+
protected readonly useMock: boolean;
|
|
84
|
+
protected readonly basePath: string;
|
|
85
|
+
protected constructor(config: IALankaGatewayConfig<TOptions>);
|
|
86
|
+
/**
|
|
87
|
+
* Resolves endpoint for request.
|
|
88
|
+
* - Absolute paths (starting with "/") are returned as-is
|
|
89
|
+
* - Relative paths are joined with basePath
|
|
90
|
+
* - Query-only strings like "?a=1" are attached to basePath
|
|
91
|
+
*/
|
|
92
|
+
protected endpoint(path?: string): string;
|
|
93
|
+
/**
|
|
94
|
+
* Joins `basePath` and the method path.
|
|
95
|
+
*/
|
|
96
|
+
private resolvePath;
|
|
97
|
+
/**
|
|
98
|
+
* Prefixes the API base URL from the host contract.
|
|
99
|
+
*
|
|
100
|
+
* Here rather than in every consumer: otherwise each consumer knows the URL
|
|
101
|
+
* and the framework does not, and a realtime plugin would have to know a
|
|
102
|
+
* specific application's build.
|
|
103
|
+
*
|
|
104
|
+
* Declaring the field and not using it would be worse than not declaring it: a
|
|
105
|
+
* declaration nothing is built from is a second truth, free to diverge from
|
|
106
|
+
* the first.
|
|
107
|
+
*
|
|
108
|
+
* An absolute URL never reaches here — `endpoint()` filters it out before the
|
|
109
|
+
* join.
|
|
110
|
+
*/
|
|
111
|
+
private withApiBase;
|
|
112
|
+
protected buildQueryParams<T extends object>(params: T): URLSearchParams;
|
|
113
|
+
protected request<TReturn = unknown>(path: string, options?: TLankaExecuteOptions<TOptions>, mockHandler?: () => Promise<TReturn>): Promise<TReturn>;
|
|
114
|
+
/**
|
|
115
|
+
* Allows to replace request implementation at runtime (e.g. feature flags / tests).
|
|
116
|
+
* If you prefer static customization - override `request()` in a subclass.
|
|
117
|
+
*/
|
|
118
|
+
protected setRequest(request: ILankaRequest<TOptions>): void;
|
|
119
|
+
protected setQueryParamsHandler(handler: TLankaQueryBuilder): void;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export { ALankaGateway as A, type IALankaGatewayConfig as I, type TLankaExecuteOptions as T, type ILankaRequest as a, type TLankaQueryBuilder as b, type TLankaQueryParams as c };
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { StoreApi } from 'zustand/vanilla';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Base abstraction for shared feature stores.
|
|
5
|
+
*
|
|
6
|
+
* A store built on this class is resolved through the shared-store locator and
|
|
7
|
+
* reused across several ViewModels.
|
|
8
|
+
*/
|
|
9
|
+
declare abstract class ALankaSharedStore<TState extends object> {
|
|
10
|
+
private readonly api;
|
|
11
|
+
private readonly createInitialState;
|
|
12
|
+
protected constructor(createInitialState: () => TState);
|
|
13
|
+
getApi(): StoreApi<TState>;
|
|
14
|
+
getState(): TState;
|
|
15
|
+
setState(partial: TState | Partial<TState> | ((state: TState) => TState | Partial<TState>), replace?: boolean): void;
|
|
16
|
+
subscribe(listener: (state: TState, prevState: TState) => void): () => void;
|
|
17
|
+
/**
|
|
18
|
+
* Back to what the store was built with.
|
|
19
|
+
*
|
|
20
|
+
* Public, and it was not: while this was `protected` only a subclass could
|
|
21
|
+
* reset, so the class style had a capability the functional one could not
|
|
22
|
+
* reach — the asymmetry `skills/parity/SKILL.md` forbids. A shared store needs
|
|
23
|
+
* explicit reset points (see the ladder in `core/README.md`), and the
|
|
24
|
+
* application that owns them is outside the class either way.
|
|
25
|
+
*/
|
|
26
|
+
reset(): void;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export { ALankaSharedStore as A };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Structural API-error shape read by consumer code.
|
|
3
|
+
*
|
|
4
|
+
* `status` is OPTIONAL: a network drop and an aborted request carry no status,
|
|
5
|
+
* and three of the six `LankaError` kinds have none.
|
|
6
|
+
*
|
|
7
|
+
* `LankaError` implements this interface, so consumers may read `status` and
|
|
8
|
+
* `errors` off any thrown framework error.
|
|
9
|
+
*/
|
|
10
|
+
interface ILankaApiError {
|
|
11
|
+
status?: number;
|
|
12
|
+
errors?: string[];
|
|
13
|
+
message?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export type { ILankaApiError as I };
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
interface ILankaFlags {
|
|
2
|
+
isMockMode?: boolean;
|
|
3
|
+
loggerEnabled?: boolean;
|
|
4
|
+
loggerGateway?: boolean;
|
|
5
|
+
loggerScenario?: boolean;
|
|
6
|
+
loggerViewModel?: boolean;
|
|
7
|
+
loggerView?: boolean;
|
|
8
|
+
loggerBootstrap?: boolean;
|
|
9
|
+
/** Comes from `import.meta.env.PROD` — silences all log output. */
|
|
10
|
+
isProduction?: boolean;
|
|
11
|
+
/** Comes from `import.meta.env.DEV` — enables development-only behaviour. */
|
|
12
|
+
isDevelopment?: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* What the package needs from the HOSTING application and cannot know itself.
|
|
17
|
+
*
|
|
18
|
+
* A package cannot import its consumer, and should not: these are product
|
|
19
|
+
* decisions belonging to whoever owns the screens and the session.
|
|
20
|
+
*
|
|
21
|
+
* The fields are REQUIRED, and that is the point. None has a sensible default —
|
|
22
|
+
* a missing message factory would mean a wrong-language string in the interface,
|
|
23
|
+
* a missing base URL an `undefined` inside a request path. So omitting one is a
|
|
24
|
+
* TYPE ERROR in the app's own barrel rather than a surprise at runtime.
|
|
25
|
+
*/
|
|
26
|
+
interface ILankaHost {
|
|
27
|
+
/**
|
|
28
|
+
* What to show when a response carried a status but no message.
|
|
29
|
+
*
|
|
30
|
+
* ONE method rather than a map of seven: which statuses the app has copy for,
|
|
31
|
+
* and what it says for the rest, is the app's business. The package knows only
|
|
32
|
+
* that it has a number and needs a sentence.
|
|
33
|
+
*/
|
|
34
|
+
httpErrorMessage(status: number): string;
|
|
35
|
+
/**
|
|
36
|
+
* API base URL — where the framework calls.
|
|
37
|
+
*
|
|
38
|
+
* In the contract because otherwise each consumer would know the URL and the
|
|
39
|
+
* framework would not, making `@lankajs/plugin-sse` impossible: it would
|
|
40
|
+
* have to know a specific application's build.
|
|
41
|
+
*
|
|
42
|
+
* Without a trailing slash — `ALankaGateway.endpoint()` normalises it.
|
|
43
|
+
*/
|
|
44
|
+
apiBaseUrl: string;
|
|
45
|
+
/**
|
|
46
|
+
* What to say when the request never reached the server.
|
|
47
|
+
*
|
|
48
|
+
* `LankaError` separates `network` from `timeout` because the interface needs
|
|
49
|
+
* different things from them. How the app phrases each is not the framework's
|
|
50
|
+
* to guess.
|
|
51
|
+
*/
|
|
52
|
+
networkErrorMessage(): string;
|
|
53
|
+
/** What to say when it arrived but no response came. */
|
|
54
|
+
timeoutErrorMessage(): string;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
interface ILankaRuntimeConfig {
|
|
58
|
+
flags?: ILankaFlags;
|
|
59
|
+
host?: ILankaHost;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type { ILankaFlags as I, ILankaHost as a, ILankaRuntimeConfig as b };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { a as ILankaScenario } from './ILankaScenarioVM-DuCyPoyT.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Metadata about a registered scenario
|
|
5
|
+
*/
|
|
6
|
+
interface ILankaScenarioMetadata {
|
|
7
|
+
scenario: ILankaScenario<unknown>;
|
|
8
|
+
name: string;
|
|
9
|
+
eventType: string;
|
|
10
|
+
dataTypeName: string;
|
|
11
|
+
isRegistered: boolean;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export type { ILankaScenarioMetadata as I };
|