@loomwork/ui-hydration 0.0.0-stage → 0.1.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 +202 -0
- package/NOTICE +8 -0
- package/README.md +398 -2
- package/dist/cache.d.ts +71 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +157 -0
- package/dist/cache.js.map +1 -0
- package/dist/chatSource.d.ts +105 -0
- package/dist/chatSource.d.ts.map +1 -0
- package/dist/chatSource.js +259 -0
- package/dist/chatSource.js.map +1 -0
- package/dist/client.d.ts +17 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +36 -0
- package/dist/client.js.map +1 -0
- package/dist/contentRefs.d.ts +78 -0
- package/dist/contentRefs.d.ts.map +1 -0
- package/dist/contentRefs.js +129 -0
- package/dist/contentRefs.js.map +1 -0
- package/dist/controller.d.ts +228 -0
- package/dist/controller.d.ts.map +1 -0
- package/dist/controller.js +454 -0
- package/dist/controller.js.map +1 -0
- package/dist/dataProvider.d.ts +53 -0
- package/dist/dataProvider.d.ts.map +1 -0
- package/dist/dataProvider.js +77 -0
- package/dist/dataProvider.js.map +1 -0
- package/dist/esm/cache.js +151 -0
- package/dist/esm/cache.js.map +1 -0
- package/dist/esm/chatSource.js +254 -0
- package/dist/esm/chatSource.js.map +1 -0
- package/dist/esm/client.js +15 -0
- package/dist/esm/client.js.map +1 -0
- package/dist/esm/contentRefs.js +119 -0
- package/dist/esm/contentRefs.js.map +1 -0
- package/dist/esm/controller.js +449 -0
- package/dist/esm/controller.js.map +1 -0
- package/dist/esm/dataProvider.js +72 -0
- package/dist/esm/dataProvider.js.map +1 -0
- package/dist/esm/index.js +15 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/invoker.js +2 -0
- package/dist/esm/invoker.js.map +1 -0
- package/dist/esm/invokers.js +310 -0
- package/dist/esm/invokers.js.map +1 -0
- package/dist/esm/loomEffectProvider.js +96 -0
- package/dist/esm/loomEffectProvider.js.map +1 -0
- package/dist/esm/params.js +207 -0
- package/dist/esm/params.js.map +1 -0
- package/dist/esm/path.js +24 -0
- package/dist/esm/path.js.map +1 -0
- package/dist/esm/plan.js +190 -0
- package/dist/esm/plan.js.map +1 -0
- package/dist/esm/projection.js +230 -0
- package/dist/esm/projection.js.map +1 -0
- package/dist/esm/provider.js +416 -0
- package/dist/esm/provider.js.map +1 -0
- package/dist/esm/repair.js +53 -0
- package/dist/esm/repair.js.map +1 -0
- package/dist/esm/reshape.js +156 -0
- package/dist/esm/reshape.js.map +1 -0
- package/dist/esm/restricted.js +54 -0
- package/dist/esm/restricted.js.map +1 -0
- package/dist/esm/scheduler.js +247 -0
- package/dist/esm/scheduler.js.map +1 -0
- package/dist/esm/select.js +18 -0
- package/dist/esm/select.js.map +1 -0
- package/dist/esm/shape.js +34 -0
- package/dist/esm/shape.js.map +1 -0
- package/dist/esm/split/client.js +309 -0
- package/dist/esm/split/client.js.map +1 -0
- package/dist/esm/split/index.js +3 -0
- package/dist/esm/split/index.js.map +1 -0
- package/dist/esm/split/server.js +488 -0
- package/dist/esm/split/server.js.map +1 -0
- package/dist/esm/split/shared.js +13 -0
- package/dist/esm/split/shared.js.map +1 -0
- package/dist/index.d.ts +29 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +63 -0
- package/dist/index.js.map +1 -0
- package/dist/invoker.d.ts +10 -0
- package/dist/invoker.d.ts.map +1 -0
- package/dist/invoker.js +3 -0
- package/dist/invoker.js.map +1 -0
- package/dist/invokers.d.ts +36 -0
- package/dist/invokers.d.ts.map +1 -0
- package/dist/invokers.js +315 -0
- package/dist/invokers.js.map +1 -0
- package/dist/loomEffectProvider.d.ts +67 -0
- package/dist/loomEffectProvider.d.ts.map +1 -0
- package/dist/loomEffectProvider.js +101 -0
- package/dist/loomEffectProvider.js.map +1 -0
- package/dist/params.d.ts +103 -0
- package/dist/params.d.ts.map +1 -0
- package/dist/params.js +221 -0
- package/dist/params.js.map +1 -0
- package/dist/path.d.ts +12 -0
- package/dist/path.d.ts.map +1 -0
- package/dist/path.js +28 -0
- package/dist/path.js.map +1 -0
- package/dist/plan.d.ts +56 -0
- package/dist/plan.d.ts.map +1 -0
- package/dist/plan.js +199 -0
- package/dist/plan.js.map +1 -0
- package/dist/projection.d.ts +122 -0
- package/dist/projection.d.ts.map +1 -0
- package/dist/projection.js +238 -0
- package/dist/projection.js.map +1 -0
- package/dist/provider.d.ts +152 -0
- package/dist/provider.d.ts.map +1 -0
- package/dist/provider.js +421 -0
- package/dist/provider.js.map +1 -0
- package/dist/repair.d.ts +18 -0
- package/dist/repair.d.ts.map +1 -0
- package/dist/repair.js +57 -0
- package/dist/repair.js.map +1 -0
- package/dist/reshape.d.ts +32 -0
- package/dist/reshape.d.ts.map +1 -0
- package/dist/reshape.js +164 -0
- package/dist/reshape.js.map +1 -0
- package/dist/restricted.d.ts +21 -0
- package/dist/restricted.d.ts.map +1 -0
- package/dist/restricted.js +61 -0
- package/dist/restricted.js.map +1 -0
- package/dist/scheduler.d.ts +82 -0
- package/dist/scheduler.d.ts.map +1 -0
- package/dist/scheduler.js +254 -0
- package/dist/scheduler.js.map +1 -0
- package/dist/select.d.ts +34 -0
- package/dist/select.d.ts.map +1 -0
- package/dist/select.js +21 -0
- package/dist/select.js.map +1 -0
- package/dist/shape.d.ts +3 -0
- package/dist/shape.d.ts.map +1 -0
- package/dist/shape.js +37 -0
- package/dist/shape.js.map +1 -0
- package/dist/split/client.d.ts +132 -0
- package/dist/split/client.d.ts.map +1 -0
- package/dist/split/client.js +313 -0
- package/dist/split/client.js.map +1 -0
- package/dist/split/index.d.ts +5 -0
- package/dist/split/index.d.ts.map +1 -0
- package/dist/split/index.js +14 -0
- package/dist/split/index.js.map +1 -0
- package/dist/split/server.d.ts +160 -0
- package/dist/split/server.d.ts.map +1 -0
- package/dist/split/server.js +496 -0
- package/dist/split/server.js.map +1 -0
- package/dist/split/shared.d.ts +6 -0
- package/dist/split/shared.d.ts.map +1 -0
- package/dist/split/shared.js +19 -0
- package/dist/split/shared.js.map +1 -0
- package/package.json +77 -3
- package/src/cache.ts +211 -0
- package/src/chatSource.ts +374 -0
- package/src/client.ts +46 -0
- package/src/contentRefs.ts +195 -0
- package/src/controller.ts +708 -0
- package/src/dataProvider.ts +141 -0
- package/src/index.ts +86 -0
- package/src/invoker.ts +14 -0
- package/src/invokers.ts +348 -0
- package/src/loomEffectProvider.ts +136 -0
- package/src/params.ts +314 -0
- package/src/path.ts +26 -0
- package/src/plan.ts +197 -0
- package/src/projection.ts +297 -0
- package/src/provider.ts +657 -0
- package/src/repair.ts +70 -0
- package/src/reshape.ts +193 -0
- package/src/restricted.ts +69 -0
- package/src/scheduler.ts +284 -0
- package/src/select.ts +59 -0
- package/src/shape.ts +39 -0
- package/src/split/client.ts +449 -0
- package/src/split/index.ts +18 -0
- package/src/split/server.ts +746 -0
- package/src/split/shared.ts +18 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding those notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
APPENDIX: How to apply the Apache License to your work.
|
|
179
|
+
|
|
180
|
+
To apply the Apache License to your work, attach the following
|
|
181
|
+
boilerplate notice, with the fields enclosed by brackets "[]"
|
|
182
|
+
replaced with your own identifying information. (Don't include
|
|
183
|
+
the brackets!) The text should be enclosed in the appropriate
|
|
184
|
+
comment syntax for the file format. We also recommend that a
|
|
185
|
+
file or class name and description of purpose be included on the
|
|
186
|
+
same "printed page" as the copyright notice for easier
|
|
187
|
+
identification within third-party archives.
|
|
188
|
+
|
|
189
|
+
Copyright [yyyy] [name of copyright owner]
|
|
190
|
+
|
|
191
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
192
|
+
you may not use this file except in compliance with the License.
|
|
193
|
+
You may obtain a copy of the License at
|
|
194
|
+
|
|
195
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
196
|
+
|
|
197
|
+
Unless required by applicable law or agreed to in writing, software
|
|
198
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
199
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
200
|
+
See the License for the specific language governing permissions and
|
|
201
|
+
limitations under the License.
|
|
202
|
+
|
package/NOTICE
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
loom-ui
|
|
2
|
+
Copyright 2026 Sushil Chhetri
|
|
3
|
+
|
|
4
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
you may not use this project except in compliance with the License.
|
|
6
|
+
You may obtain a copy of the License at
|
|
7
|
+
|
|
8
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
package/README.md
CHANGED
|
@@ -1,3 +1,399 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @loomwork/ui-hydration
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Resolves an Experience's provider handles through loom's transient effects and publishes them as
|
|
4
|
+
one loom projection, so a renderer can bind to `values.<handle>` / `status.<handle>` and redraw as
|
|
5
|
+
they arrive. It is the provider plane of the pipeline: `@loomwork/ui-core` compiles an Experience to a
|
|
6
|
+
`Blueprint`; this package answers what the Blueprint's bindings need, in-process or split across a
|
|
7
|
+
trust boundary; `@loomwork/ui-renderer-lit` and `@loomwork/ui-renderer-terminal` read the resulting
|
|
8
|
+
projection state. Hydration is **read-only and log-free** — it never emits a durable event; drift
|
|
9
|
+
repair is the app's own loom execution.
|
|
10
|
+
|
|
11
|
+
## Install / import
|
|
12
|
+
|
|
13
|
+
```jsonc
|
|
14
|
+
// package.json of a consumer checked out beside this repo (the packages are not published)
|
|
15
|
+
"@loomwork/ui-hydration": "link:../loom-ui/packages/hydration"
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// Node / server: executes providers — imports loom's sandbox (QuickJS WASM) and MCP plugin.
|
|
20
|
+
import { HydrationController, handleInventory, serveHandles } from "@loomwork/ui-hydration";
|
|
21
|
+
|
|
22
|
+
// Browser: split hydration only. Never import the root entry from a browser bundle.
|
|
23
|
+
import { SplitHydrationController, HydrationStore } from "@loomwork/ui-hydration/client";
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
## Key concepts
|
|
27
|
+
|
|
28
|
+
- **Two entries, one trust boundary.** The root entry (`@loomwork/ui-hydration`) is server-side: it
|
|
29
|
+
executes providers, so its module graph reaches loom's sandbox and MCP plugin. The client entry
|
|
30
|
+
(`@loomwork/ui-hydration/client`, `src/client.ts`) carries only what split hydration needs in a
|
|
31
|
+
browser: the split controller, the loader contract, the store, and param checks — nothing that
|
|
32
|
+
imports the sandbox, MCP, `@loomwork/effect-runtime` or a Node builtin. `clientEntry.test.ts` walks
|
|
33
|
+
the *built* client bundle to enforce this, and keeps it under a 56 KiB size budget (the
|
|
34
|
+
minified ESM bundle). The client entry also carries the pure helpers other client-side packages need
|
|
35
|
+
(`terminalSteps`, `stepReferences`, `topoWaves`, `jsonPathSelect`, `outputShapeHashOf`, a
|
|
36
|
+
client-safe mirror of the sandbox's shape hash, parity-tested). `@loomwork/ui-planner`,
|
|
37
|
+
`renderer-terminal`, `renderer-lit` and `core` import ONLY `@loomwork/ui-hydration/client`;
|
|
38
|
+
`clientConsumers.test.ts` fails if any of them reaches the root entry, QuickJS or the MCP plugin,
|
|
39
|
+
by source import or built module graph.
|
|
40
|
+
- **The hydration projection.** Every handle publishes to one loom projection
|
|
41
|
+
(`HYDRATION_PROJECTION_ID = "loom-ui.hydration"`, version `HYDRATION_PROJECTION_VERSION`) that a
|
|
42
|
+
`HydrationStore` wraps; a compiled Blueprint's bindings read `values.<handle>` / `status.<handle>`
|
|
43
|
+
from it. A handle's `HandleState` is `"pending" | "ready" | "stale" | "error" | "empty"`.
|
|
44
|
+
- **In-process vs. split.** `HydrationController` executes providers directly through an injected
|
|
45
|
+
`TransientInvoker` (any loom `EffectRuntime` satisfies it structurally).
|
|
46
|
+
`SplitHydrationController` (client) talks to a `HandleLoader` — the app's transport — which calls
|
|
47
|
+
`serveHandle`/`serveHandles` (server) over that boundary; the server answers with the exact same
|
|
48
|
+
`HandleLoadResult` shape the in-process controller publishes, so the two paths cannot disagree.
|
|
49
|
+
- **`scopeInvoker` and `principal`.** When the invoker is a planner `scopeInvoker` (a `ctx.decide`
|
|
50
|
+
scope), do not configure `principal`: the scope already carries the execution's principal and the
|
|
51
|
+
call is refused otherwise.
|
|
52
|
+
- **Bounded cache (U17).** `new HydrationCache(clock, { maxEntries })` evicts the least recently
|
|
53
|
+
used result past `maxEntries` (default `DEFAULT_CACHE_MAX_ENTRIES` = 10,000; a hit or a set
|
|
54
|
+
refreshes an entry; a non-positive-integer cap throws `RangeError`). An evicted result is a miss,
|
|
55
|
+
exactly like an expired one: the next load calls the provider again. Before this the cache only
|
|
56
|
+
dropped an entry on a re-read after its TTL, so a long-lived ambient host grew without bound.
|
|
57
|
+
- **Scheduling.** Both controllers schedule by priority: P0 loads immediately, P1 batches within a
|
|
58
|
+
short window, P2 waits for `markVisible`. An identical step (same tool, same arguments) runs once
|
|
59
|
+
per pass no matter how many handles share it.
|
|
60
|
+
- **Params, time tokens, drift.** A step declares which argument names a load may override
|
|
61
|
+
(`MCPToolNode.overridable`); `$now`/`$now±<n><s|m|h|d|w>` tokens resolve once per pass against an
|
|
62
|
+
injected clock; drift (an input schema pin that moved, or an output that no longer reshapes or
|
|
63
|
+
fits its contract) is reported through `onDrift`, never repaired here.
|
|
64
|
+
|
|
65
|
+
## API
|
|
66
|
+
|
|
67
|
+
### The projection and its store
|
|
68
|
+
|
|
69
|
+
- `HYDRATION_PROJECTION_ID`, `HYDRATION_PROJECTION_VERSION`, `HYDRATION_SET_EVENT` — the fixed
|
|
70
|
+
projection id/version and the internal set event it folds.
|
|
71
|
+
- `hydrationProjection()`, `HydrationState`, `HandleSlice`, `HandleState`, `HandleStatus`,
|
|
72
|
+
`HydrationScope` — the loom `Projection` itself and its state shape. Isolation is by (experience,
|
|
73
|
+
principal, audience): `HydrationScope.audience` (`"broadcast"`) is its own state, and
|
|
74
|
+
`HydrationAudienceMismatchError` is thrown when a controller's `audience` differs from its
|
|
75
|
+
store's.
|
|
76
|
+
- `HydrationStore` — wraps a `projectionRuntime` and exposes `.state()` and `.runtimeSubscribe(fn)`
|
|
77
|
+
for a renderer or `watch` to read/observe.
|
|
78
|
+
- `isReservedHandle(handle)` — true for a handle name the projection itself reserves.
|
|
79
|
+
|
|
80
|
+
### Running providers (server / Node only)
|
|
81
|
+
|
|
82
|
+
- `HydrationController` — in-process controller. Constructor takes `ControllerOptions` (`invoker`,
|
|
83
|
+
`store`, `clock?`, `setTimer?`, `batchWindowMs?`, `concurrency?`, `maxConcurrentCalls?`,
|
|
84
|
+
`principal?`, `cache?`, `viewer?`, `audience?`, `readPolicy?`, `declaredToolClasses?`, `undeclared?`,
|
|
85
|
+
`onDrift?`, `outputKinds?`, `defaultParams?`, `inputDrift?`,
|
|
86
|
+
`argumentHook?`, `contentRefArgs` (required), `acceptContentRefParam?`, `reshapers?`,
|
|
87
|
+
`providers?`, `captureSample?`). Methods: `load(exp)`, `recompose(next)`, `markVisible(handle)`,
|
|
88
|
+
`setParams(params, scope?)`, `invalidate(handle?)`, `inFlight()`, `dispose()`, plus `setViewer(viewer)`,
|
|
89
|
+
`append(handle, chunk)` and `publishValue(handle, value, { state?, kind? })` — the ones that can
|
|
90
|
+
fail return `{ findings: Finding[] }` or a `{ ok: false, finding }` refusal.
|
|
91
|
+
- **Content-reference arguments (U9b).** `contentRefArgs` is REQUIRED on `ControllerOptions` and
|
|
92
|
+
`ServeOptions`: the table `contentRefArgsFrom(capabilities)` builds (persist it with the
|
|
93
|
+
capability set; `parseContentRefTable` re-reads it), or the explicit opt-out `"none"` (an
|
|
94
|
+
experience with media nodes then reports `media-content-refs-unguarded`). Before ANY call a step's
|
|
95
|
+
marked argument must be absent / null, a `{{steps.…}}` reference to an output its tool declares to
|
|
96
|
+
be a content reference, or a param the host accepted; otherwise the step is refused
|
|
97
|
+
(`media-arg-literal` / `media-arg-unproven-ref`, or `media-arg-param-refused` for a param the gate
|
|
98
|
+
refused: three distinct codes on `onFinding`) and the call is NOT made. Behaviour change: when
|
|
99
|
+
every failed step was such a refusal, the outcome's finding (and what a split client receives) is
|
|
100
|
+
the public code `content-ref-refused` with one fixed message, no longer the opaque
|
|
101
|
+
`provider-failed`; it says nothing of which argument, step or whether a reference exists. A
|
|
102
|
+
refusal mixed with another failure stays `provider-failed`. A param the gate refuses used to be
|
|
103
|
+
reported as `media-arg-literal` and is now `media-arg-param-refused`. The same check runs BEFORE
|
|
104
|
+
every cache lookup (in-process and split), asking the gate again: a cached value is never served
|
|
105
|
+
to a principal whose gate refuses, nor after a revocation, and a result a param filled is cached
|
|
106
|
+
per principal only, never globally. A param that fills a marked argument (in-process `setParams`,
|
|
107
|
+
or a split client's params, which are client input) is refused unless `acceptContentRefParam({
|
|
108
|
+
handle, stepId, toolName, kind?, param, value, principalId?, principal? }) => boolean` returns
|
|
109
|
+
`true`. The gate MUST check that the value was ISSUED TO `principalId` (e.g. keep the `MediaRef`s
|
|
110
|
+
`acceptMediaAttach` accepted for each principal and return
|
|
111
|
+
`issued.get(principalId)?.has(value.address)`) and is still current (a revoked share answers
|
|
112
|
+
`false`). `null` at a marked path is stripped before the call. The `argumentHook` runs after the
|
|
113
|
+
check and is the host's own word. A tool whose schema mentions the marker but was unreadable
|
|
114
|
+
(`"unknown"`) has every replaced param gated and every literal string refused.
|
|
115
|
+
- `executeProvider` — runs one `ProviderSpec`'s execution graph and reshaping; what both controllers
|
|
116
|
+
and `serveHandle`/`serveHandles` call underneath.
|
|
117
|
+
- `ExecuteOptions`, `ProviderOutcome`, `DriftRecord`, `InputDriftMode` — the execution options and
|
|
118
|
+
outcome/drift shapes `executeProvider` and `onDrift` use.
|
|
119
|
+
- `DriftReport` — what `ControllerOptions.onDrift` actually receives: `{ experienceId, versionHash,
|
|
120
|
+
handle, spec, drift: readonly DriftRecord[], findings, proceeded }` — one or more drift records
|
|
121
|
+
for the handle, plus the provider spec that drifted and whether the load retried and published
|
|
122
|
+
(`inputDrift: "report-and-proceed"`).
|
|
123
|
+
- `HandleLoadResult` — what every load resolves to and what both controllers publish: `{ state:
|
|
124
|
+
"ready", value, finding? } | { state: "stale" | "error", finding } | { state: "empty", finding }`.
|
|
125
|
+
- `HydrationCache`, `CacheVariant` (`params`, `kinds`, `contracts` — the bound kinds'
|
|
126
|
+
`ValueContract.version`s are folded into the key; unversioned contracts leave keys unchanged, so
|
|
127
|
+
call `invalidate()` after changing one) — the per-principal, per-params cache
|
|
128
|
+
`HydrationController` uses by default (inject your own, or a fresh one per controller).
|
|
129
|
+
- `limitInvoker`, `memoInvoker`, `signalInvoker` — `TransientInvoker` wrappers: cap concurrency,
|
|
130
|
+
de-duplicate identical calls within one pass, and bind an `AbortSignal`. `memoInvoker` keys on the
|
|
131
|
+
principal AND every other call option except `signal` (e.g. a stamped execution identity `as`, or
|
|
132
|
+
the `label` that names the transient execution when `as` is absent — an authorizer may grant by
|
|
133
|
+
either), so calls made as different executions never share a result.
|
|
134
|
+
- `defineReshapers(entries)`, `jsonPathSelect(value, path)`, `outputShapeHashOf(value)` — build a
|
|
135
|
+
`ReshaperRegistry` for `reshaping.engine: "Named"`, run a JSONPath reshape, and hash a value's
|
|
136
|
+
structural shape for pinning `ProviderSpec.outputShapeHash`.
|
|
137
|
+
- `stepReferences`, `terminalSteps`, `topoWaves` — inspect an execution graph's `{{steps.…}}`
|
|
138
|
+
references, find its terminal step(s), and compute its topological waves.
|
|
139
|
+
- `DEFAULT_TIME_RANGE`, `checkParams` — the default `{ from: "$now-24h", to: "$now" }` params, and a
|
|
140
|
+
params validity check.
|
|
141
|
+
|
|
142
|
+
### The data plane: data-provider kinds (server / Node only)
|
|
143
|
+
|
|
144
|
+
A provider step used to be an MCP tool call and nothing else. A step now names a **kind**
|
|
145
|
+
(`step.kind`; absent = `mcp`, the original) and hydration runs it through the `DataProvider`
|
|
146
|
+
registered for that kind. A loom app whose data lives in a loom effect (a `tool$`, a read-model
|
|
147
|
+
query, an A2A call) binds it directly, without wrapping it as an MCP server.
|
|
148
|
+
|
|
149
|
+
- `DataProvider` — `{ kind, invoke(call, invoker, opts) }`. `call` is `{ stepId, target, args,
|
|
150
|
+
expectedPin }`: `target` is the step's `toolName` (its id WITHIN the kind), `args` the prepared
|
|
151
|
+
arguments (params, time tokens, templates, hook already applied), `expectedPin` the step's
|
|
152
|
+
`toolSchemaHash`. `invoke` runs the target THROUGH the transient `invoker` it is handed with
|
|
153
|
+
`opts` (signal, principal, label) — so the invoker's authoriser, budget and per-principal identity
|
|
154
|
+
stay in force, the read path stays log-free, and a kind never sees the runtime.
|
|
155
|
+
- **The pin is the contract.** `invoke` MUST compare `expectedPin` with the target's current pin and
|
|
156
|
+
throw `TypedFailure("schemaDrift", { observedHash })` BEFORE running the target. That is what
|
|
157
|
+
makes drift a `DriftRecord` (`kind: "input"`, `providerKind` set for a non-MCP kind), what
|
|
158
|
+
`remapOperation` repairs, and what `inputDrift: "report-and-proceed"` retries on. A kind that
|
|
159
|
+
cannot say what its target's contract is must not be registered: there is no unpinned kind.
|
|
160
|
+
`checkDataProviderConformance(provider, { target, pin, invoker })` returns the violations (empty =
|
|
161
|
+
conformant), including a violation if a stale pin reached a direct call to `target`; it is a TEST
|
|
162
|
+
HELPER — `defineDataProviders` cannot enforce the rule, so run it in a kind's own tests.
|
|
163
|
+
- `defineDataProviders(providers)` → `DataProviderRegistry` (`has`/`get`/`kinds`) — composition-time
|
|
164
|
+
only, no discovery, no later registration; a malformed or duplicate kind id throws. Pass it as
|
|
165
|
+
`providers` to `HydrationController`, `serveHandle(s)` or `executeProvider`. Default
|
|
166
|
+
(`DEFAULT_DATA_PROVIDERS`): MCP only, byte-identical to before kinds.
|
|
167
|
+
- `mcpProvider` — the first registered kind: `mcp.invoke` with `expectedPin` as `expectedSchemaHash`
|
|
168
|
+
(the plugin verifies it per call).
|
|
169
|
+
- `loomEffectProvider({ runtime, effects })` — the second kind, `loom-effect` (`LOOM_EFFECT_KIND`):
|
|
170
|
+
a step whose `toolName` is a registered loom effect id, invoked via `invokeTransient`. Hydration
|
|
171
|
+
is a read path (per viewer load, unmemoised, retried), so at CONSTRUCTION every listed effect is
|
|
172
|
+
checked against `runtime` (`has` / `modeOf` / `kindOf`): it must be registered, explicitly `mode:
|
|
173
|
+
"ambient"` (ADR 0073 — an UNDECLARED mode is refused, since loom itself only refuses an explicit
|
|
174
|
+
`journaled` one), and not a `human` / `llm` kind. `effects` is an ALLOWLIST: a stored or
|
|
175
|
+
planner-authored step can name any string. Each entry is `{ inputSchema }` (the pin is computed
|
|
176
|
+
with `effectPin(schema)` = `jsonSchemaHash` of its JSON-Schema form, so editing the effect's zod
|
|
177
|
+
schema drifts every stored step with no edit to the map) or `{ pin }` (HOST-ASSERTED: nothing
|
|
178
|
+
checks it against the effect). Use `effectPin` also when building a planner
|
|
179
|
+
`CapabilityRef.toolSchemaHash`, so both come from one source. An unlisted target fails
|
|
180
|
+
`tool-targetUnregistered` and nothing runs; `mcp.invoke` and the sandbox effect cannot be listed.
|
|
181
|
+
A target that itself throws `schemaDrift` surfaces as `tool-targetFailed` (`TARGET_FAILED`), never
|
|
182
|
+
as pin drift — only the provider's own pin check emits `schemaDrift`, or a repair would flip the
|
|
183
|
+
pin forever. An effect call is not memoised within a pass (`memoInvoker` coalesces `mcp.invoke`
|
|
184
|
+
only); memoising `ambient` effects is a possible follow-up. A persisted experience must cross
|
|
185
|
+
version boundaries only through the log: an older in-memory `executeProvider` / `seal` that
|
|
186
|
+
predates kinds would run a kinded step as an MCP call.
|
|
187
|
+
- `ArgumentHookContext.kind` — the step's provider kind (absent for MCP): a hook keyed on `toolName`
|
|
188
|
+
must also read it, since MCP tool `x` and loom effect `x` are different targets.
|
|
189
|
+
- An unregistered kind fails the WHOLE provider as `provider-kind-unregistered` before any step runs
|
|
190
|
+
(never a throw, no partial calls); served publicly as a fixed text naming neither kind nor target.
|
|
191
|
+
|
|
192
|
+
A step's class identity is `toolKey(step)` (core): the bare `toolName` for MCP — every class
|
|
193
|
+
recorded before kinds still applies — and `<kind>:<toolName>` otherwise, so `toolClasses` stickiness
|
|
194
|
+
and `effectiveClasses` cover every kind and a loom effect never shares a class with a same-named MCP
|
|
195
|
+
tool.
|
|
196
|
+
|
|
197
|
+
### Split hydration — server half
|
|
198
|
+
|
|
199
|
+
- `handleInventory(exp, opts: InventoryOptions): { inventory, findings }` — the client-safe
|
|
200
|
+
inventory: per bound handle, its priority, the overridable param names, and an opaque, keyed
|
|
201
|
+
revision.
|
|
202
|
+
- `InventoryOptions` — `{ revisionOf: RevisionFn, viewer?, audience?, declaredToolClasses?, undeclared? }`.
|
|
203
|
+
- `createServeSurface(policy?: ReadPolicy): ServeSurface` — `handleInventory` / `serveHandle` /
|
|
204
|
+
`serveHandles` with the read policy bound once (prefer it to the free functions); it also
|
|
205
|
+
exposes the merged `policy`.
|
|
206
|
+
- `RevisionFn` — `(handle: string, fingerprint: string) => string`. MUST be a keyed hash (e.g. an
|
|
207
|
+
HMAC over a server secret) — never the provider's own content fingerprint, which a client could
|
|
208
|
+
confirm a guessed provider against offline; `handleInventory` throws if it returns a non-string,
|
|
209
|
+
an empty string, or a string containing the fingerprint.
|
|
210
|
+
- `serveHandle(exp, request, opts: ServeOptions)`, `serveHandles(exp, requests, opts: ServeOptions)`
|
|
211
|
+
— answer one or many `HandleLoadRequest`s; every request is treated as untrusted and re-checked
|
|
212
|
+
server-side. `ServeOptions` extends `ExecuteOptions` (minus `params`/`kinds`) with `maxBatch?`
|
|
213
|
+
(default `DEFAULT_MAX_BATCH`, 256) and `maxConcurrentCalls?` (default
|
|
214
|
+
`DEFAULT_MAX_CONCURRENT_CALLS`, 32).
|
|
215
|
+
- `publicFinding(handle, finding)` — the public form of a finding a client may see (fixed code,
|
|
216
|
+
handle name only — never a tool, argument, script or exception text).
|
|
217
|
+
|
|
218
|
+
### Split hydration — client half (also on `@loomwork/ui-hydration/client`)
|
|
219
|
+
|
|
220
|
+
- `SplitHydrationController` — same scheduling as `HydrationController`, over a `HandleInventory`,
|
|
221
|
+
loading through an injected `HandleLoader`. Methods: `load(inventory)`, `recompose(next)`,
|
|
222
|
+
`markVisible(handle)`, `setParams(params, scope?)`, `invalidate(handle?)`, `inFlight()`,
|
|
223
|
+
`dispose()`, plus `publishValue(handle, value, { state?, kind })` (`kind` is required here).
|
|
224
|
+
- `HandleLoader` — the app's transport: `load(request, signal)` and `loadMany(requests, signal)`
|
|
225
|
+
(both required — loads the scheduler starts together go out as one `loadMany` round trip).
|
|
226
|
+
- `HandleInventory`, `HandleRef`, `HandleLoadRequest`, `SplitControllerOptions` — the wire shapes.
|
|
227
|
+
- `handleOfPath(path): string | undefined`, `HYDRATION_VALUES_PREFIX` — client entry only. Maps a
|
|
228
|
+
widget's binding path (`values.<handle>`) back to its hydration handle: the exact inverse of
|
|
229
|
+
`@loomwork/ui-core`'s `hydrationPath`. `undefined` unless the path is `values.` plus a non-empty
|
|
230
|
+
handle (the bare prefix, `status.<handle>` and near-misses like `valuesh` all give `undefined`);
|
|
231
|
+
everything after the first `values.` is the handle, spelled exactly as `hydrationPath` spelled
|
|
232
|
+
it (a handle containing `.` round-trips here but will not resolve at render time, because
|
|
233
|
+
binding paths are resolved by splitting on `.` — keep handles dot-free). The browser takes these
|
|
234
|
+
from here so the client entry never pulls core's runtime graph into a bundle. The prefix is
|
|
235
|
+
defined in both packages (core cannot import hydration); a type-level `satisfies` and a test pin
|
|
236
|
+
them equal.
|
|
237
|
+
|
|
238
|
+
### Repair (app-side, durable)
|
|
239
|
+
|
|
240
|
+
- `SCHEMA_REPAIRED`, `SchemaRepairedPayload` — the userspace event name and payload an app emits
|
|
241
|
+
after applying a repair, from inside a loom execution.
|
|
242
|
+
- `remapOperation(spec, drift)` — turns an INPUT drift record into the `Operation` that repairs it
|
|
243
|
+
(refuses output/reshape drift with `repair-needs-planner` — that needs `@loomwork/ui-planner`'s
|
|
244
|
+
`reviseForDrift`).
|
|
245
|
+
|
|
246
|
+
## Examples
|
|
247
|
+
|
|
248
|
+
In-process hydration (illustrative — not a compiling snippet):
|
|
249
|
+
|
|
250
|
+
```ts
|
|
251
|
+
import { HydrationController, HydrationStore, SCHEMA_REPAIRED, remapOperation } from "@loomwork/ui-hydration";
|
|
252
|
+
|
|
253
|
+
const store = new HydrationStore(projectionRuntime, {
|
|
254
|
+
experienceId: experience.experienceId,
|
|
255
|
+
principalId: viewer.id,
|
|
256
|
+
});
|
|
257
|
+
const controller = new HydrationController({
|
|
258
|
+
invoker: effectRuntime, // any EffectRuntime satisfies TransientInvoker
|
|
259
|
+
store,
|
|
260
|
+
principal: { id: viewer.id, chain: viewer.principalChain },
|
|
261
|
+
onDrift: (report) => durableRepair(report), // the app's own loom execution
|
|
262
|
+
});
|
|
263
|
+
controller.load(experience); // P0 now, P1 in ~50ms, P2 on markVisible
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
Split hydration, client side (illustrative):
|
|
267
|
+
|
|
268
|
+
```ts
|
|
269
|
+
import { HydrationStore, SplitHydrationController } from "@loomwork/ui-hydration/client";
|
|
270
|
+
|
|
271
|
+
const split = new SplitHydrationController({
|
|
272
|
+
store,
|
|
273
|
+
loader: {
|
|
274
|
+
load: (request, signal) => postJson("/hydrate", request, signal),
|
|
275
|
+
loadMany: (requests, signal) => postJson("/hydrate/batch", requests, signal),
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
split.load(inventory); // fetched from the server's handleInventory(exp, { revisionOf })
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## Notable behaviours and guarantees
|
|
282
|
+
|
|
283
|
+
- **A handle that feeds a media widget is always contract-checked (U9).** When the controller is
|
|
284
|
+
given `outputKinds` and a handle's bound nodes resolve to `Image`/`Audio`/`Video`/`File`, the load
|
|
285
|
+
runs as `outputCheck: "contract"` whatever the provider declared: a shape pin passes any value of
|
|
286
|
+
the pinned shape (inline bytes or a locator included), and what is published reaches the store, a
|
|
287
|
+
seen log and a split-hydration payload. A value that is not a `MediaRef` is drift
|
|
288
|
+
(`output-contract-drift`), never published. A restricted media handle is never invoked, as for any
|
|
289
|
+
restricted handle. Kinds are resolved through the catalogue when `outputKinds` is given AND by
|
|
290
|
+
node type alone: a node whose `type` is a framework media kind marks its handle as media even with
|
|
291
|
+
NO `outputKinds`, so forgetting it fails closed (a `media-output-kinds-missing` warning says to
|
|
292
|
+
pass it). The split server (`serveHandle`/`serveHandles`) applies the same rule, including its
|
|
293
|
+
cache variant. Resolving a ref to a URL is NOT hydration's job: it is the host's `MediaResolver`,
|
|
294
|
+
called by the renderer (see `@loomwork/ui-core` "Media").
|
|
295
|
+
|
|
296
|
+
- **A handle that feeds a chat transcript is always contract-checked, and never a stream (U8).**
|
|
297
|
+
Like media: a node of type `Chat` (or a kind that resolves to it) marks its handle, the provider
|
|
298
|
+
is checked against the `ChatValue` contract whatever `outputCheck` it declared (controller AND
|
|
299
|
+
split server), and a text chunk (`append`) cannot overwrite a transcript. A live reply therefore
|
|
300
|
+
rides the transcript VALUE (a `ChatFeed` republishes it), with `pending` status while it streams,
|
|
301
|
+
rather than `appendChunk` on a string handle; the bounds of `appendChunk` still apply inside the
|
|
302
|
+
feed.
|
|
303
|
+
- **`connectConversation` (U8, `@loomwork/ui-hydration/client`)** is the browser's read half over loom's
|
|
304
|
+
conversation routes (`GET {base}/history`, `GET {base}/events`, ADR 0122 D6). The transport is
|
|
305
|
+
injected (`fetch`; `openStream(url)` so the host builds the `EventSource` with its credentials),
|
|
306
|
+
it imports no loom package, and it folds history pages and SSE frames into a `ChatFeed`,
|
|
307
|
+
publishing the result through `publish` (`SplitHydrationController.publishValue` can check only
|
|
308
|
+
that the handle is one the server LISTED; it cannot know the listed handle is a `Chat`, so the
|
|
309
|
+
`kind` it checks against is the page author's own claim). (`publishChat(controller, handle)`
|
|
310
|
+
writes the handle THROUGH the controller's gate: `publishValue` refuses a handle the experience
|
|
311
|
+
does not bind or the server did not list, a restricted one, one that is not a `Chat`, and a value
|
|
312
|
+
that fails the `ChatValue` contract; `pending` while a reply streams, `ready` once durable).
|
|
313
|
+
`start()` loads the newest page then opens the stream after its cursor; `loadOlder()` follows the
|
|
314
|
+
feed's cursor, one load at a time; **reasoning** (ADR 0133) rides the same connection: the
|
|
315
|
+
`reasoning` / `reasoning-partial` events go to the feed (which ignores them unless
|
|
316
|
+
`ChatFeed({ reasoning })`), and with `reasoning: true` here and a durable feed,
|
|
317
|
+
`loadReasoning(cursor)` fetches `GET {base}/reasoning?cursor=` for ONE reply, once, only when the host
|
|
318
|
+
calls it (route a Chat's `LOAD_REASONING` action to it, like `LOAD_OLDER`); a 404/403 is a settled
|
|
319
|
+
"none for you", a transport failure is a fixed finding and retryable, never the error text; a `reconnect` event reopens from the feed's resume cursor after
|
|
320
|
+
at least 250 ms (never 0), consecutive failures back off (the first wait is `reconnectMs`, 2 s,
|
|
321
|
+
then doubling to 30 s; a stream that stayed up 5 s or more starts the streak afresh), and a failed
|
|
322
|
+
first page is retried the same way instead of giving up. Failures are FIXED findings
|
|
323
|
+
(`history-unavailable`, `frame-invalid`, `frame-too-large`, `stream-error`, and
|
|
324
|
+
`reasoning-unavailable` for the reasoning fetch): no status code,
|
|
325
|
+
response body or exception text is carried, and an `error` event's body is not read. Sending is
|
|
326
|
+
NOT here: it is the host's authenticated write-back (`createWriteBack`, `@loomwork/ui-core` "Chat and
|
|
327
|
+
inputs").
|
|
328
|
+
- **A host-declared tool class floors it too.** `declaredToolClasses` (on `HydrationController`,
|
|
329
|
+
`handleInventory` and `serveHandle(s)`; `@loomwork/ui-core`'s `DeclaredToolClasses`) makes a handle
|
|
330
|
+
whose provider calls a declared tool at least that class, even when no stored node carries one.
|
|
331
|
+
**Bind it once with `createServeSurface(policy)`**: it returns `handleInventory`, `serveHandle`
|
|
332
|
+
and `serveHandles` with a core `ReadPolicy` (`{ declaredToolClasses, undeclared }`) applied to
|
|
333
|
+
every call, so the inventory and the loader cannot disagree because one call forgot the table. A
|
|
334
|
+
per-call `declaredToolClasses` / `undeclared` only ADDS (`{}` or `undefined` cannot lower it). The
|
|
335
|
+
free `handleInventory` / `serveHandle(s)` take the table per call and are the low-level API.
|
|
336
|
+
`undeclared: "deny"` refuses every handle whose provider calls a tool the table does not list (no
|
|
337
|
+
table = an empty one: all tool-bound handles), for every viewer, until the tool is declared; a
|
|
338
|
+
per-call table cannot list a new tool once deny is bound. `HydrationController` takes the SAME
|
|
339
|
+
`readPolicy` (`new HydrationController({ readPolicy })`; the separate `declaredToolClasses` /
|
|
340
|
+
`undeclared` options join it raise-only), so a host passes ONE policy to the controller and to
|
|
341
|
+
`createServeSurface`.
|
|
342
|
+
- **A classified handle is never fetched for a viewer who may not see it (fail-closed).** Pass
|
|
343
|
+
`viewer: { clearance }` (`@loomwork/ui-core`'s `Viewer`) to `HydrationController`, `handleInventory`
|
|
344
|
+
and `serveHandle`/`serveHandles`. A handle bound by any node whose `sensitivity` is above the
|
|
345
|
+
clearance is not planned, not cached and its provider is never invoked, and the controller writes
|
|
346
|
+
NOTHING to the store for it: no value and no status, so a store projected to a client does not
|
|
347
|
+
even name the handle (a stale entry is removed). The viewer is snapshotted at construction;
|
|
348
|
+
`setViewer` re-plans (drops now-restricted values, aborts their in-flight loads, loads newly
|
|
349
|
+
allowed ones). Token streams obey the same gate: `ctl.append(handle, chunk)` (and `store.append`
|
|
350
|
+
once a controller fronts the store) refuses a handle that is restricted for the viewer
|
|
351
|
+
(`restricted`), not bound (`stream-unbound`), or — with `outputKinds` — not a `StreamingText`
|
|
352
|
+
widget (`stream-kind-mismatch`); nothing is written. `handleInventory` omits it (the client learns
|
|
353
|
+
neither name nor revision); the server answers a client that names it anyway exactly as for a
|
|
354
|
+
handle the experience does not bind (`unknown-handle`, no existence oracle) and logs the full
|
|
355
|
+
`restricted` finding to `onFinding`. No viewer = no clearance, with one `viewer-not-supplied` warn
|
|
356
|
+
when anything is classified; unclassified handles are unaffected. Use the same viewer you pass to
|
|
357
|
+
`compile`, and one store per viewer.
|
|
358
|
+
|
|
359
|
+
- **`empty` is an answer, not an error.** A step whose `{{steps.<id>.output.<path>}}` indexes past
|
|
360
|
+
the end of a real (but short) upstream array publishes `empty` — a warn (`upstream-empty`), no
|
|
361
|
+
value, never cached, never drift, never repaired. Every other miss (a bad key, `null` mid-path, a
|
|
362
|
+
step with no output) stays `unresolved-step-reference` → `error`.
|
|
363
|
+
- **Relative time is read once per pass.** `$now`/`$now±<n><unit>` resolve against one clock read,
|
|
364
|
+
so two steps in the same load asking for "the last day" agree; anything else starting `$now` is
|
|
365
|
+
refused (`invalid-time-token`) before it reaches a tool.
|
|
366
|
+
- **Calls are capped and de-duplicated.** `maxConcurrentCalls` bounds the whole controller (or one
|
|
367
|
+
server batch); beneath it, one step memo per pass runs an identical step once no matter how many
|
|
368
|
+
handles share it, ref-counted so one caller's abort never cancels another caller's still-pending
|
|
369
|
+
wait on the same call.
|
|
370
|
+
- **A provider never reaches a browser.** Split hydration ships only the inventory
|
|
371
|
+
(`handleInventory`) and answers requests (`serveHandle`); a client only ever sees a finding's
|
|
372
|
+
PUBLIC form (fixed code + handle name) — the full finding (tool, step, script, exception text)
|
|
373
|
+
reaches only the server's own `onFinding` hook.
|
|
374
|
+
- **Drift is reported, never repaired here.** `onDrift` fires with a `DriftReport`
|
|
375
|
+
(`{ experienceId, versionHash, handle, spec, drift: readonly DriftRecord[], findings, proceeded
|
|
376
|
+
}`) — one or more typed `DriftRecord`s for the handle, not a single record. Applying a fix
|
|
377
|
+
(`remapOperation` for input drift, `@loomwork/ui-planner`'s `reviseForDrift` for output drift) and
|
|
378
|
+
emitting `SCHEMA_REPAIRED` is the app's job, inside its own durable execution. Nothing in this
|
|
379
|
+
package dedupes or bounds repairs — an app must dedupe on `(versionHash, handle)` and cap
|
|
380
|
+
attempts itself.
|
|
381
|
+
- **A `scriptError` payload is untrusted, unlogged text.** A reshaping script's own exception can
|
|
382
|
+
echo the raw output; loom-ui classifies drift by the failure's typed channel alone and never
|
|
383
|
+
copies that text into a finding message, handle status or drift record.
|
|
384
|
+
- **Recompose keeps shared handles.** `controller.load(nextExperience)` (or
|
|
385
|
+
`SplitHydrationController.recompose`) aborts and drops handles the new artifact no longer binds
|
|
386
|
+
and keeps the ones still bound — no full re-hydrate on every revision.
|
|
387
|
+
|
|
388
|
+
## See also
|
|
389
|
+
|
|
390
|
+
- [../core/README.md](../core/README.md) — the Experience Model and `compile` this package
|
|
391
|
+
hydrates.
|
|
392
|
+
- [../planner/README.md](../planner/README.md) — `reviseForDrift` repairs the output drift this
|
|
393
|
+
package reports.
|
|
394
|
+
- [../renderer-lit/README.md](../renderer-lit/README.md) and
|
|
395
|
+
[../renderer-terminal/README.md](../renderer-terminal/README.md) — read the projection this
|
|
396
|
+
package publishes.
|
|
397
|
+
- [../../docs/reference/hydration.md](../../docs/reference/hydration.md) — the full hydration
|
|
398
|
+
options reference and split-hydration wiring.
|
|
399
|
+
- [../../README.md](../../README.md) — root overview.
|