lanka 1.3.0 → 2.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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/dist/ILankaReadableVM-BoPzHEPV.d.ts +72 -0
  3. package/dist/{LankaScenarioLocator-CAENLhDO.d.ts → LankaScenarioLocator-Dki2Es3O.d.ts} +2 -2
  4. package/dist/{LankaSharedStoreLocator-zS2kLu-S.d.ts → LankaSharedStoreLocator-jsz_gnUm.d.ts} +1 -1
  5. package/dist/_extend/index.d.ts +228 -5
  6. package/dist/_extend/index.js +137 -8
  7. package/dist/_extend/index.js.map +1 -1
  8. package/dist/_internal/index.d.ts +3 -3
  9. package/dist/_internal/index.js +9 -3
  10. package/dist/_internal/index.js.map +1 -1
  11. package/dist/{activeRuntime-BYd2D6Lx.d.ts → activeRuntime-BO-qY0yY.d.ts} +50 -3
  12. package/dist/bootstrap/index.d.ts +6 -6
  13. package/dist/bootstrap/index.js +7 -8
  14. package/dist/{chunk-D27MREPB.js → chunk-7DQUF2QR.js} +2 -2
  15. package/dist/{chunk-UJEC7H6K.js → chunk-BHQ2SQ7P.js} +2 -2
  16. package/dist/chunk-EYDMBI5T.js +550 -0
  17. package/dist/chunk-EYDMBI5T.js.map +1 -0
  18. package/dist/{chunk-C2HP7CRD.js → chunk-FII3PW2G.js} +2 -2
  19. package/dist/{chunk-DTO27QFR.js → chunk-H5TIUKRK.js} +2 -2
  20. package/dist/{chunk-NDCJBO47.js → chunk-O5EUTNR6.js} +2 -2
  21. package/dist/{chunk-RKYKK6MN.js → chunk-RUMUFHSL.js} +2 -2
  22. package/dist/{chunk-UBGXDTXC.js → chunk-SZVNPK24.js} +11 -13
  23. package/dist/chunk-SZVNPK24.js.map +1 -0
  24. package/dist/chunk-UDP6IXDS.js +17 -0
  25. package/dist/chunk-UDP6IXDS.js.map +1 -0
  26. package/dist/{chunk-5MAQVBI2.js → chunk-UMPSR4US.js} +3 -3
  27. package/dist/{chunk-BGVDPDX4.js → chunk-XGMXT4XZ.js} +20 -3
  28. package/dist/chunk-XGMXT4XZ.js.map +1 -0
  29. package/dist/config/index.js +3 -3
  30. package/dist/{createLanka-NrlvN-WQ.d.ts → createLanka-x4oSMs7P.d.ts} +2 -2
  31. package/dist/{createLankaScope-BiFxNQgl.d.ts → createLankaScope-DomNbEvb.d.ts} +1 -1
  32. package/dist/errors/index.js +3 -3
  33. package/dist/gateway/index.js +9 -9
  34. package/dist/index.d.ts +5 -5
  35. package/dist/index.js +8 -9
  36. package/dist/locator/index.d.ts +2 -2
  37. package/dist/locator/index.js +2 -2
  38. package/dist/logger/index.js +3 -3
  39. package/dist/mock/index.js +3 -3
  40. package/dist/scenario/index.d.ts +25 -0
  41. package/dist/scenario/index.js +6 -8
  42. package/dist/scenario/index.js.map +1 -1
  43. package/dist/stream/index.d.ts +5 -5
  44. package/dist/viewmodel/index.d.ts +137 -27
  45. package/dist/viewmodel/index.js +94 -126
  46. package/dist/viewmodel/index.js.map +1 -1
  47. package/package.json +3 -4
  48. package/skills/lanka-core/SKILL.md +18 -16
  49. package/skills/lanka-core/reference.md +135 -58
  50. package/skills/lanka-packages/SKILL.md +94 -22
  51. package/dist/chunk-B7EYIAW7.js +0 -216
  52. package/dist/chunk-B7EYIAW7.js.map +0 -1
  53. package/dist/chunk-BGVDPDX4.js.map +0 -1
  54. package/dist/chunk-O5ROO7QF.js +0 -315
  55. package/dist/chunk-O5ROO7QF.js.map +0 -1
  56. package/dist/chunk-UBGXDTXC.js.map +0 -1
  57. /package/dist/{chunk-D27MREPB.js.map → chunk-7DQUF2QR.js.map} +0 -0
  58. /package/dist/{chunk-UJEC7H6K.js.map → chunk-BHQ2SQ7P.js.map} +0 -0
  59. /package/dist/{chunk-C2HP7CRD.js.map → chunk-FII3PW2G.js.map} +0 -0
  60. /package/dist/{chunk-DTO27QFR.js.map → chunk-H5TIUKRK.js.map} +0 -0
  61. /package/dist/{chunk-NDCJBO47.js.map → chunk-O5EUTNR6.js.map} +0 -0
  62. /package/dist/{chunk-RKYKK6MN.js.map → chunk-RUMUFHSL.js.map} +0 -0
  63. /package/dist/{chunk-5MAQVBI2.js.map → chunk-UMPSR4US.js.map} +0 -0
@@ -1,10 +1,10 @@
1
1
  <!-- Generated from core/GUIDE.md by scripts/skills.mjs. Edit the guide. -->
2
2
 
3
- > **`lanka@1.3.0`** — this document describes that version.
3
+ > **`lanka@2.0.0`** — this document describes that version.
4
4
  >
5
- > Install: `npm install lanka react zustand` (the peers are not optional; only npm adds a missing one for you).
5
+ > Install: `npm install lanka zustand` (the peers are not optional; only npm adds a missing one for you).
6
6
  >
7
- > Complete code, compiled and run in CI: [core/_playground/playground.test.tsx](https://github.com/lankajs/lanka/blob/main/core/_playground/playground.test.tsx)
7
+ > Complete code, compiled and run in CI: [core/_playground/playground.test.ts](https://github.com/lankajs/lanka/blob/main/core/_playground/playground.test.ts)
8
8
 
9
9
  # lanka — user guide
10
10
 
@@ -27,8 +27,31 @@ If you want to know _why_ the framework is shaped this way, read
27
27
  > required: [ARCHITECTURE.md](https://github.com/lankajs/lanka/blob/main/ARCHITECTURE.md) separates the five things the
28
28
  > machine checks from the many that are advice you can take or leave.
29
29
 
30
+ ## When to reach for this
31
+
32
+ Every other package here is adopted one at a time; this one is the framework, so
33
+ the question is the other way round. Reach for lanka when an application has
34
+ SERVER STATE that several screens care about — a list one screen changes and
35
+ another shows, a session two screens read, a fact that has to travel without one
36
+ screen importing another. That is what the gateway, the ViewModel and the
37
+ scenario bus are for, and it is the shape that gets expensive when it is left to
38
+ grow by itself.
39
+
40
+ Do not reach for it for a form, a landing page, or a screen that loads once and
41
+ sits still: a `useState` and a `fetch` are the right size for those, and this
42
+ framework would be four layers around nothing.
43
+
44
+ It is not an application shell either. Your host framework — Next, React Router,
45
+ TanStack Start, Astro, Expo — keeps routing, rendering and caching;
46
+ [ARCHITECTURE.md](https://github.com/lankajs/lanka/blob/main/ARCHITECTURE.md#inside-another-framework) draws the line.
47
+
48
+ It binds no UI framework of its own: core imports no UI library at all, and
49
+ reading a ViewModel from a screen is one of the five packages in
50
+ `modules/bindings/`.
51
+
30
52
  ## Contents
31
53
 
54
+ - [When to reach for this](#when-to-reach-for-this)
32
55
  - [Install](#install)
33
56
  - [Your first application](#your-first-application)
34
57
  - [The layers, and which way imports go](#the-layers-and-which-way-imports-go)
@@ -60,16 +83,28 @@ If you want to know _why_ the framework is shaped this way, read
60
83
  ## Install
61
84
 
62
85
  ```bash
63
- npm install lanka react react-dom zustand
64
- pnpm add lanka react react-dom zustand
65
- yarn add lanka react react-dom zustand
66
- bun add lanka react react-dom zustand
86
+ npm install lanka zustand
87
+ pnpm add lanka zustand
88
+ yarn add lanka zustand
89
+ bun add lanka zustand
67
90
  ```
68
91
 
69
- **The peers are listed on purpose.** `react` 19 and `zustand` 5 are peer
70
- dependencies, and only npm installs those for you. Under pnpm, yarn or bun a
71
- missing peer is a warning at install time and a resolution error at build time —
72
- name them once and the question never comes up.
92
+ **The peer is listed on purpose.** `zustand` 5 is core's one peer dependency,
93
+ and only npm installs a missing peer for you. Under pnpm, yarn or bun it is a
94
+ warning at install time and a resolution error at build time — name it once and
95
+ the question never comes up.
96
+
97
+ **No UI framework is in that line, and that is not an omission.** Core imports no
98
+ UI library at all: a ViewModel is a store, a gateway is a function, and neither
99
+ knows what renders. Reading a ViewModel from a screen is one package more, the
100
+ one for your framework:
101
+
102
+ ```bash
103
+ npm install @lankajs/react # or @lankajs/vue, @lankajs/svelte, @lankajs/solid, @lankajs/angular
104
+ ```
105
+
106
+ All five publish the same name — `useLankaVM` — so the guide you read for one
107
+ is the guide for the next.
73
108
 
74
109
  > [!NOTE]
75
110
  > Any package manager works, and nothing in the framework knows which one you
@@ -599,16 +634,16 @@ more protected member, `toLifecycleHooks`, is the framework's own reading of
599
634
 
600
635
  ### Config reference
601
636
 
602
- | Field | Meaning |
603
- | ---------------------------------- | --------------------------------------------------- |
604
- | `name` | Shown in logs and devtools. Required |
605
- | `states` | The initial state. Omit for a stateless ViewModel |
606
- | `createActions` | Receives the context, returns the actions |
607
- | `gateways` / `services` | An object or a factory; reachable as `gateways.x` |
608
- | `scenarioHandlers` | `{ scenario, handler }` pairs, bound at bootstrap |
609
- | `enhancers` | Store enhancers, zustand style |
637
+ | Field | Meaning |
638
+ | ---------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
639
+ | `name` | Shown in logs and devtools. Required |
640
+ | `states` | The initial state. Omit for a stateless ViewModel |
641
+ | `createActions` | Receives the context, returns the actions |
642
+ | `gateways` / `services` | An object or a factory; reachable as `gateways.x` |
643
+ | `scenarioHandlers` | `{ scenario, handler }` pairs, bound at bootstrap |
644
+ | `enhancers` | Store enhancers, zustand style |
610
645
  | `onInit` / `onReset` | Lifecycle hooks over the same context. Declaring either enrols the ViewModel with scenario bootstrap: `onInit` runs when bootstrap binds it, `onReset` when the instance — or a lazy ViewModel — is disposed |
611
- | `enableAccessTrackingOptimization` | Default on; see [common mistakes](#common-mistakes) |
646
+ | `enableAccessTrackingOptimization` | Default on; see [common mistakes](#common-mistakes) |
612
647
 
613
648
  ### Using one in a component
614
649
 
@@ -626,6 +661,48 @@ The hook re-renders a component only for the keys it actually **read**. That is
626
661
  usually free performance, and it has one blind spot, described at the end of this
627
662
  guide.
628
663
 
664
+ ### A selector that builds its answer
665
+
666
+ Every binding also takes a selector, and then the selector decides instead of
667
+ the tracking. A selector that **builds** what it answers has a trap in it:
668
+
669
+ ```ts
670
+ useLankaVM(missionVM, (state) => ({ title: state.title, status: state.status }));
671
+ ```
672
+
673
+ That object is new on every call, so it is never identical to the previous one,
674
+ and a binding compares selections by identity. The reader is therefore woken by
675
+ every change in the ViewModel — including the keys the selector exists to
676
+ ignore. A selector answering a **primitive** never has the problem, which is what
677
+ makes this quiet: the shape that is free and the shape that repaints on
678
+ everything look the same on the page.
679
+
680
+ `createLankaShallowHold` is the comparison that fixes it. It answers the
681
+ **previous** object while nothing in the selection moved, one level deep — own
682
+ keys, same count, `Object.is` on each value, arrays included:
683
+
684
+ ```ts
685
+ import { createLankaShallowHold } from "lanka/viewmodel";
686
+
687
+ const hold = createLankaShallowHold<{ title: string; status: string }>();
688
+
689
+ const view = useLankaVM(missionVM, (state) => hold({ title: state.title, status: state.status }));
690
+ ```
691
+
692
+ One hold per reader, created once beside the ViewModel read — never shared
693
+ between two components, because the answer it holds belongs to whoever selected
694
+ it.
695
+
696
+ > [!NOTE]
697
+ > In React, write `useLankaShallow` from `@lankajs/react` instead. It is the same
698
+ > comparison as a hook, and React needs one: a component re-runs the hook on
699
+ > every render, so the holding has to survive a render while the selector stays
700
+ > the current one.
701
+
702
+ A selection with a **nested** object wants a selector that picks the leaves.
703
+ Comparing deeper would mean walking a state of unknown size on every read, which
704
+ is the cost a selector was taken to avoid.
705
+
629
706
  ## Choosing a coordination tool
630
707
 
631
708
  When "A must affect B", one question decides it: **do A and B co-own a thing, or
@@ -788,15 +865,15 @@ Things worth knowing:
788
865
  - **A failing handler cannot take the dispatch down**, and that holds for an
789
866
  async one too. A handler is typed `(data) => void`, but TypeScript assigns a
790
867
  `Promise<void>` to a void return position — so `async () => { await refetch();
791
- }` compiles with nothing to warn about, and "refetch when the stream
868
+ }` compiles with nothing to warn about, and "refetch when the stream
792
869
  reconnects" is the ordinary shape rather than an exotic one. Its rejection is
793
870
  caught and written to the scenario log, not left to surface as an unhandled
794
871
  rejection in whatever ran next.
795
872
 
796
- What the framework cannot do is decide what the failure MEANT. A log line is a
797
- diagnostic, not a retry and not a message on a screen — so an action called
798
- from a handler should still own its own failure, because the handler returns
799
- `void` and has nowhere to put one.
873
+ What the framework cannot do is decide what the failure MEANT. A log line is a
874
+ diagnostic, not a retry and not a message on a screen — so an action called
875
+ from a handler should still own its own failure, because the handler returns
876
+ `void` and has nowhere to put one.
800
877
 
801
878
  - **Middleware returns a decision** — `"pass"` or `{ stop: reason }` — never
802
879
  `next()`. A middleware that forgot to call `next()` would make the event vanish
@@ -838,28 +915,28 @@ the whole cost a form library exists to remove — and here you get it for free.
838
915
 
839
916
  ### Add a form library when the form grows its own behaviour
840
917
 
841
- | Reach for one when | Why the ViewModel stops being enough |
842
- | -------------------------------------------------------- | ---------------------------------------------------- |
843
- | a field ARRAY — rows the user adds and removes | rows force nesting, and nesting is not tracked |
844
- | validation on every keystroke | the whole object re-checked per character |
845
- | one field's validity depends on another's value | written by hand, it drifts |
846
- | `touched` / `dirty` / blur as behaviour, not decoration | three booleans per input in a screen's state |
847
- | the screen is server-rendered WITH values | see the warning at the end of this section |
918
+ | Reach for one when | Why the ViewModel stops being enough |
919
+ | ------------------------------------------------------- | ---------------------------------------------- |
920
+ | a field ARRAY — rows the user adds and removes | rows force nesting, and nesting is not tracked |
921
+ | validation on every keystroke | the whole object re-checked per character |
922
+ | one field's validity depends on another's value | written by hand, it drifts |
923
+ | `touched` / `dirty` / blur as behaviour, not decoration | three booleans per input in a screen's state |
924
+ | the screen is server-rendered WITH values | see the warning at the end of this section |
848
925
 
849
926
  Not on the list: how many forms the application has. A big application of flat
850
927
  forms needs nothing; one wizard with dynamic rows does.
851
928
 
852
929
  ### The boundary
853
930
 
854
- | | Owner |
855
- | --- | --- |
856
- | values, `touched`/`dirty`, focus, per-input messages | the **form** |
857
- | `isSubmitting` | the form — unless it is visible outside the form (a global overlay) |
858
- | `defaultValues` | the ViewModel: the server's version, held as `server` |
859
- | calling the gateway, `trigger`, clearing a draft, navigating | the **ViewModel** |
860
- | an input's ASYNCHRONOUS check | a ViewModel action — a resolver may not call a gateway |
861
- | failures with an address | pass through the ViewModel to the form |
862
- | failures without one | the ViewModel's own state |
931
+ | | Owner |
932
+ | ------------------------------------------------------------ | ------------------------------------------------------------------- |
933
+ | values, `touched`/`dirty`, focus, per-input messages | the **form** |
934
+ | `isSubmitting` | the form — unless it is visible outside the form (a global overlay) |
935
+ | `defaultValues` | the ViewModel: the server's version, held as `server` |
936
+ | calling the gateway, `trigger`, clearing a draft, navigating | the **ViewModel** |
937
+ | an input's ASYNCHRONOUS check | a ViewModel action — a resolver may not call a gateway |
938
+ | failures with an address | pass through the ViewModel to the form |
939
+ | failures without one | the ViewModel's own state |
863
940
 
864
941
  The last row is the interesting one, and the two before it are why a form cannot
865
942
  be left to talk to the network itself.
@@ -874,11 +951,11 @@ no adapter to write and nothing declared twice.
874
951
  Keep the INPUT schema apart from the response ones. A form given the response
875
952
  schema asks for an `id` and an `updatedAt` the user does not have:
876
953
 
877
- | Schema | Read by | Changes when |
878
- | ------------- | -------------------------------- | -------------------- |
879
- | `orderWire` | the gateway, on the way in | the backend changes |
880
- | `order` | the gateway, the domain check | the application does |
881
- | `orderInput` | **the form's resolver AND the gateway's payload check** | what a person may type changes |
954
+ | Schema | Read by | Changes when |
955
+ | ------------ | ------------------------------------------------------- | ------------------------------ |
956
+ | `orderWire` | the gateway, on the way in | the backend changes |
957
+ | `order` | the gateway, the domain check | the application does |
958
+ | `orderInput` | **the form's resolver AND the gateway's payload check** | what a person may type changes |
882
959
 
883
960
  ### A failure that knows which input it belongs to
884
961
 
@@ -937,9 +1014,9 @@ your decision, not a framework's.
937
1014
  const order = await gateways.orderGateway.update(id, values);
938
1015
 
939
1016
  set({ server: order, serverChangedAt: null }); // 1. mark your own write
940
- trigger(orderUpdated, { order }); // 2. announce it, WITH the data
941
- services.cache?.write(["order", id], order); // 3. tell a cache, if you have one
942
- return { ok: true, data: order }; // 4. the form is still mounted here
1017
+ trigger(orderUpdated, { order }); // 2. announce it, WITH the data
1018
+ services.cache?.write(["order", id], order); // 3. tell a cache, if you have one
1019
+ return { ok: true, data: order }; // 4. the form is still mounted here
943
1020
  // 5. navigate — after this returns
944
1021
  ```
945
1022
 
@@ -1053,14 +1130,14 @@ lanka at all: a Standard Schema in, `ILankaFieldError[]` out.
1053
1130
  the wire is server-sent events, a WebSocket, a GraphQL subscription or a gRPC
1054
1131
  server stream.
1055
1132
 
1056
- | Name | What it is |
1057
- | --------------------------------- | ---------------------------------------------------------------- |
1133
+ | Name | What it is |
1134
+ | --------------------------------- | --------------------------------------------------------------------- |
1058
1135
  | `ILankaServerEventTransport` | the port: `isSupported`, `connect`, `disconnect`, `on`, `onReconnect` |
1059
- | `ALankaStreamBridge` | a wire event → one of your scenarios |
1060
- | `createLankaStreamBridge` | the same, written by calling |
1061
- | `createLankaStreamTriggerContext` | the "this came from outside" marker |
1062
- | `ALankaStreamTransport` | the base a transport extends: dispatch, and the reconnect ladder |
1063
- | `lankaStream` | the plugin: bridges attached, lifetime owned |
1136
+ | `ALankaStreamBridge` | a wire event → one of your scenarios |
1137
+ | `createLankaStreamBridge` | the same, written by calling |
1138
+ | `createLankaStreamTriggerContext` | the "this came from outside" marker |
1139
+ | `ALankaStreamTransport` | the base a transport extends: dispatch, and the reconnect ladder |
1140
+ | `lankaStream` | the plugin: bridges attached, lifetime owned |
1064
1141
 
1065
1142
  You normally reach these through a protocol package —
1066
1143
  [`@lankajs/plugin-sse`](https://github.com/lankajs/lanka/blob/main/plugins/sse/GUIDE.md),
@@ -1103,7 +1180,7 @@ the backoff ladder with its attempt ceiling and one auth refresh, and
1103
1180
  `onReconnect`. A subclass writes `open` and `close`, and reports through three
1104
1181
  handlers — `opened`, `received`, `lost`. Nothing else, and deliberately: the bug
1105
1182
  this shape is prone to is resetting the attempt counter where reconnection
1106
- *starts* rather than where it *succeeds*, after which the ceiling exists, reads
1183
+ _starts_ rather than where it _succeeds_, after which the ceiling exists, reads
1107
1184
  as a guard, and can never fire.
1108
1185
 
1109
1186
  ## The locator
@@ -1310,7 +1387,7 @@ npx lanka-skills sync
1310
1387
 
1311
1388
  ### The modules and plugins
1312
1389
 
1313
- Twenty-one more packages solve problems you may or may not have — four wire
1390
+ Thirty-seven more packages solve problems you may or may not have — four wire
1314
1391
  protocols, retry policy, optimistic updates, list handling, storage,
1315
1392
  prefetching. The table of
1316
1393
  "add it when" is in [ARCHITECTURE.md](https://github.com/lankajs/lanka/blob/main/ARCHITECTURE.md#adopting-the-packages),
@@ -1,11 +1,11 @@
1
1
  ---
2
2
  name: lanka-packages
3
- description: Choose which lanka package solves a problem, and find its skill. Use when a lanka application needs a capability it does not have yet — storage, realtime, caching, prefetching, optimistic updates, list handling, validation, cookies, start-up stages or a devtool — or when deciding whether something belongs in application code at all.
3
+ description: Choose which lanka package solves a problem, and find its skill. Use when a lanka application needs a capability it does not have yet — reading a ViewModel from a React, Vue, Svelte, Solid or Angular screen, storage and its engine, realtime over SSE or WebSocket, GraphQL or gRPC-Web, a read cache, response validation, prefetching, optimistic updates, list handling, cookies, start-up stages, a devtool, or living inside Next, Nuxt, SvelteKit or Expo — or when deciding whether something belongs in application code at all.
4
4
  license: MIT
5
5
  metadata:
6
6
  author: lankajs
7
7
  package: lanka
8
- version: "1.3.0"
8
+ version: "2.0.0"
9
9
  ---
10
10
 
11
11
  # lanka — which package, and what it costs
@@ -19,24 +19,91 @@ has a skill of its own with the detail.
19
19
 
20
20
  ## By the problem
21
21
 
22
- | The problem | The package | Its skill |
23
- | ------------------------------------------------------- | ------------------------------- | ---------------------------- |
24
- | screens, data, state, events | `lanka` | `lanka-core` |
25
- | stale data after several refreshes; bursts; polling | `@lankajs/async` | `lanka-async` |
26
- | a button that must not double-fire; instant feedback | `@lankajs/optimistic` | `lanka-optimistic` |
27
- | a table: sort, filter, paginate, stop re-rendering rows | `@lankajs/collection` | `lanka-collection` |
28
- | localStorage, IndexedDB, encryption, persisted state | `@lankajs/storage` | `lanka-storage` |
29
- | avatars and thumbnails that reload or flicker | `@lankajs/blob-cache` | `lanka-blob-cache` |
30
- | cookies; stale caches after a new build shipped | `@lankajs/browser` | `lanka-browser` |
31
- | validating responses with zod / valibot | `@lankajs/zod`, `@lankajs/valibot` | `lanka-zod`, `lanka-valibot` |
32
- | retry, idempotency, timeouts, auth refresh, CSRF | `@lankajs/plugin-http` | `lanka-http` |
33
- | realtime updates pushed by the server | `@lankajs/plugin-sse` | `lanka-sse` |
34
- | warming data or route code before the user asks | `@lankajs/plugin-prefetch` | `lanka-prefetch` |
35
- | ordered start-up stages that can redirect | `@lankajs/plugin-bootstrap-steps` | `lanka-bootstrap-steps` |
36
- | seeing what the bus and the logger did | `@lankajs/plugin-devtools` | `lanka-devtools` |
37
- | the `@lanka_di` wiring and its build-time check | `@lankajs/tool-di` | `lanka-di` |
38
- | architectural boundaries as lint rules | `@lankajs/tool-eslint` | `lanka-eslint` |
39
- | testing, and benchmarking | `@lankajs/tool-testing` | `lanka-testing` |
22
+ | The problem | The package | Its skill |
23
+ | ------------------------------------------------------- | --------------------- | ------------------ |
24
+ | screens, data, state, events | `lanka` | `lanka-core` |
25
+ | stale data after several refreshes; bursts; polling | `@lankajs/async` | `lanka-async` |
26
+ | a button that must not double-fire; instant feedback | `@lankajs/optimistic` | `lanka-optimistic` |
27
+ | a table: sort, filter, paginate, stop re-rendering rows | `@lankajs/collection` | `lanka-collection` |
28
+ | localStorage, IndexedDB, encryption, persisted state | `@lankajs/storage` | `lanka-storage` |
29
+ | avatars and thumbnails that reload or flicker | `@lankajs/blob-cache` | `lanka-blob-cache` |
30
+ | cookies; stale caches after a new build shipped | `@lankajs/browser` | `lanka-browser` |
31
+ | Next, React Router, TanStack Start or Expo around lanka | `@lankajs/host` | `lanka-host` |
32
+
33
+ ## Reading a ViewModel from a screen
34
+
35
+ Core imports no UI library, so this is the one package a rendering application
36
+ always adds. Install the one for its framework and no other they are
37
+ alternatives, not layers:
38
+
39
+ | The framework | The package | Its skill |
40
+ | ------------------- | ------------------ | --------------- |
41
+ | React, React Native | `@lankajs/react` | `lanka-react` |
42
+ | Vue, Nuxt | `@lankajs/vue` | `lanka-vue` |
43
+ | Svelte, SvelteKit | `@lankajs/svelte` | `lanka-svelte` |
44
+ | Solid | `@lankajs/solid` | `lanka-solid` |
45
+ | Angular | `@lankajs/angular` | `lanka-angular` |
46
+
47
+ All five publish `useLankaVM`, and the access tracking is core's rather than
48
+ each binding's — so what a screen re-renders for is the same answer in every
49
+ framework. What differs is what the call ANSWERS: the state itself in React, a
50
+ `ShallowRef` in Vue, getters in Svelte, an `Accessor` in Solid, a `Signal` in
51
+ Angular.
52
+
53
+ Nothing else needs one. Gateways, scenarios, the locator and
54
+ `viewModel.getState()` are plain calls with no view in them, and they run on a
55
+ server unchanged.
56
+
57
+ ## What goes on the wire
58
+
59
+ Everything here is a plugin: core declares the extension point, and the plugin is
60
+ registered with `lanka.use(...)`.
61
+
62
+ | The problem | The package | Its skill |
63
+ | ------------------------------------------------- | --------------------------------- | ----------------------- |
64
+ | retry, idempotency, timeouts, auth refresh, CSRF | `@lankajs/plugin-http` | `lanka-http` |
65
+ | the server pushes updates, one way | `@lankajs/plugin-sse` | `lanka-sse` |
66
+ | the wire has to carry traffic BOTH ways | `@lankajs/plugin-websocket` | `lanka-websocket` |
67
+ | the API is GraphQL, with or without subscriptions | `@lankajs/plugin-graphql` | `lanka-graphql` |
68
+ | the API is gRPC and the client is a browser | `@lankajs/plugin-grpc` | `lanka-grpc` |
69
+ | warming data or route code before the user asks | `@lankajs/plugin-prefetch` | `lanka-prefetch` |
70
+ | ordered start-up stages that can redirect | `@lankajs/plugin-bootstrap-steps` | `lanka-bootstrap-steps` |
71
+ | seeing what the bus and the logger did | `@lankajs/plugin-devtools` | `lanka-devtools` |
72
+
73
+ SSE and WebSocket sit behind the SAME port, so the choice is one line in bootstrap
74
+ and not a rewrite. Take SSE unless the client has to SEND on the same connection.
75
+
76
+ ## Pick exactly one from a family
77
+
78
+ Four decisions where the framework refuses to choose for you, because the right
79
+ answer is whichever library your application already has. Each family binds one
80
+ port, so every member publishes the same surface under a different vendor name.
81
+
82
+ | The decision | The members | Its skill |
83
+ | ------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
84
+ | which schema library validates responses | `@lankajs/zod`, `@lankajs/valibot`, `@lankajs/arktype`, `@lankajs/yup`, `@lankajs/typebox`, `@lankajs/effect` | `lanka-zod`, `lanka-valibot`, … |
85
+ | …and the app ended up with two of them | `@lankajs/any-schema` | `lanka-any-schema` |
86
+ | which read cache sits under the ViewModels | `@lankajs/tanstack-query`, `@lankajs/nanostores-query` | `lanka-tanstack-query`, `lanka-nanostores-query` |
87
+ | where `@lankajs/storage` writes off the web | `@lankajs/mmkv`, `@lankajs/react-native-async-storage`, `@lankajs/secure-store`, `@lankajs/unstorage` | `lanka-mmkv`, `lanka-secure-store`, … |
88
+ | which UI framework reads the ViewModels | `@lankajs/react`, `@lankajs/vue`, `@lankajs/svelte`, `@lankajs/solid`, `@lankajs/angular` | `lanka-react`, `lanka-vue`, … |
89
+
90
+ The recommended member, when there is one: `@lankajs/zod` for schemas, and
91
+ `@lankajs/tanstack-query` for the read cache. Take another only for a reason you
92
+ can name — usually that the library is already in the application.
93
+
94
+ A storage engine is the one family you can skip entirely: in a browser
95
+ `@lankajs/storage` brings its own adapters. Install one when the app runs on a
96
+ device (`@lankajs/mmkv`, or `@lankajs/secure-store` for a token) or on a server
97
+ (`@lankajs/unstorage`).
98
+
99
+ ## Build-time and test-time
100
+
101
+ | The problem | The package | Its skill |
102
+ | ------------------------------------------------------ | ----------------------- | --------------- |
103
+ | the `@lanka_di` wiring and its build-time check | `@lankajs/tool-di` | `lanka-di` |
104
+ | architectural boundaries as lint rules | `@lankajs/tool-eslint` | `lanka-eslint` |
105
+ | testing, and benchmarking | `@lankajs/tool-testing` | `lanka-testing` |
106
+ | keeping these skills matched to the installed versions | `@lankajs/tool-skills` | `lanka-skills` |
40
107
 
41
108
  ## Module or plugin, and why you should care
42
109
 
@@ -49,11 +116,16 @@ has a skill of its own with the detail.
49
116
 
50
117
  ## What NOT to install
51
118
 
52
- - Both `@lankajs/zod` and `@lankajs/valibot`. Pick one; the point is that the choice
53
- is visible in your dependency list.
119
+ - Two members of one family. Pick one; the point is that the choice is visible in
120
+ your dependency list. `@lankajs/any-schema` is the exception, and only for an
121
+ application that already carries two schema libraries.
122
+ - A second view binding. One application renders with one UI framework, and
123
+ installing two puts two copies of `useLankaVM` in the import list.
54
124
  - `@lankajs/plugin-prefetch` before the app is measurably slow somewhere. It is
55
125
  three tiers of machinery for a problem you may not have.
56
126
  - `@lankajs/blob-cache` for images whose URL can change. It never checks freshness.
127
+ - A read cache before two screens read the same resource. One screen with its own
128
+ ViewModel already has everywhere it needs to keep that data.
57
129
 
58
130
  ## Getting the skills
59
131