caspian-utils 0.2.4 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/docs/pulsepoint.md +197 -176
- package/package.json +1 -1
package/dist/docs/pulsepoint.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: PulsePoint Runtime Guide
|
|
3
|
-
description: Use this page when the task mentions PulsePoint, performance, rerenders, slow inputs or searches, `pp.state`, `pp.ref`, `pp.effect`, `pp-ref`, `pp-style`, `pp-for`, portals, `pp-reset-scroll`, SPA navigation, or `public/js/pp-reactive-v2.js`. Read "PulsePoint Is Not JSX" before writing templates and "High-performance authoring" before changing runtime reconciliation.
|
|
3
|
+
description: Use this page when the task mentions PulsePoint, performance, rerenders, slow inputs or searches, `pp.state`, `pp.ref`, `pp.effect`, `pp-ref`, `pp-style`, `pp-for`, portals, `pp-reset-scroll`, SPA navigation, or `public/js/pp-reactive-v2.js`. Read "PulsePoint Is Not JSX" before writing templates and "High-performance authoring" before changing runtime reconciliation.
|
|
4
4
|
related:
|
|
5
5
|
title: Related docs
|
|
6
6
|
description: Read the components, routing, data-fetching, and project-structure docs alongside the PulsePoint runtime contract.
|
|
@@ -771,172 +771,193 @@ Behavior to rely on:
|
|
|
771
771
|
- A boundary absorbs at most five errors without an intervening `reset()`, then logs and stops catching. This stops a fallback that itself throws from spinning the render loop. `reset()` re-arms it.
|
|
772
772
|
- Event handler errors are not routed to boundaries. Handle those with `try`/`catch` inside the handler, which matches React.
|
|
773
773
|
|
|
774
|
-
## High-performance authoring
|
|
775
|
-
|
|
776
|
-
PulsePoint renders synchronously when a component requests an update. That makes state ownership and component boundaries part of the performance contract: the runtime can optimize reconciliation, but it cannot decide that an authored state change was unnecessary.
|
|
777
|
-
|
|
778
|
-
### State means "render required"
|
|
779
|
-
|
|
780
|
-
Use `pp.state(...)` when changing the value must affect at least one render-facing behavior:
|
|
781
|
-
|
|
782
|
-
- template text, attributes, lists, or conditional visibility
|
|
783
|
-
- a bound child prop or provided context value
|
|
784
|
-
- a memo, effect, or other render dependency that must follow the value
|
|
785
|
-
- authoritative data returned from an RPC call
|
|
786
|
-
|
|
787
|
-
Use `pp.ref(...)` when a mutable value must persist but changing it should not render:
|
|
788
|
-
|
|
789
|
-
- debounce and timeout handles
|
|
790
|
-
- request generations or stale-response tokens
|
|
791
|
-
- pagination cursors that are not displayed
|
|
792
|
-
- previous values and imperative integration handles
|
|
793
|
-
- transient search text used only to build a later RPC payload
|
|
794
|
-
|
|
795
|
-
A ref mutation is intentionally invisible to the template. Do not replace state with a ref when the UI is expected to update.
|
|
796
|
-
|
|
797
|
-
### Debounce frequency and render cost are different
|
|
798
|
-
|
|
799
|
-
A debounce answers "how often should this work start?" It does not answer "how much work happens when it starts?"
|
|
800
|
-
|
|
801
|
-
This shape can still stall a large owner:
|
|
802
|
-
|
|
803
|
-
```html
|
|
804
|
-
<script>
|
|
805
|
-
const [query, setQuery] = pp.state("");
|
|
806
|
-
|
|
807
|
-
function onSearch(value) {
|
|
808
|
-
clearTimeout(onSearch.timer);
|
|
809
|
-
onSearch.timer = setTimeout(() => setQuery(value), 300);
|
|
810
|
-
}
|
|
811
|
-
|
|
812
|
-
pp.effect(() => {
|
|
813
|
-
loadRows(query);
|
|
814
|
-
}, [query]);
|
|
815
|
-
</script>
|
|
816
|
-
```
|
|
817
|
-
|
|
818
|
-
When the timer fires, `setQuery` asks the whole owning component to render even if `query` never appears in its markup. If that owner also contains a large list, dialogs, forms, providers, or many nested components, typing that resumes near the timer can compete with the render.
|
|
819
|
-
|
|
820
|
-
When query text exists only for an RPC request, keep the query and timer in refs, debounce the request itself, and render only the accepted result:
|
|
821
|
-
|
|
822
|
-
```html
|
|
823
|
-
<section>
|
|
824
|
-
<input
|
|
825
|
-
type="search"
|
|
826
|
-
placeholder="Search products"
|
|
827
|
-
oninput="scheduleSearch(event.target.value)"
|
|
828
|
-
/>
|
|
829
|
-
|
|
830
|
-
<template pp-for="row in rows">
|
|
831
|
-
<article key="{row.id}">{row.label}</article>
|
|
832
|
-
</template>
|
|
833
|
-
|
|
834
|
-
<script>
|
|
835
|
-
const [rows, setRows] = pp.state([]);
|
|
836
|
-
const query = pp.ref("");
|
|
837
|
-
const timer = pp.ref(null);
|
|
838
|
-
const generation = pp.ref(0);
|
|
839
|
-
|
|
840
|
-
function scheduleSearch(value) {
|
|
841
|
-
query.current = value;
|
|
842
|
-
const requestGeneration = ++generation.current;
|
|
843
|
-
|
|
844
|
-
clearTimeout(timer.current);
|
|
845
|
-
timer.current = setTimeout(
|
|
846
|
-
() => search(requestGeneration),
|
|
847
|
-
300
|
|
848
|
-
);
|
|
849
|
-
}
|
|
850
|
-
|
|
851
|
-
async function search(requestGeneration) {
|
|
852
|
-
const response = await pp.rpc(
|
|
853
|
-
"search_products",
|
|
854
|
-
{ query: query.current },
|
|
855
|
-
{ abortPrevious: true }
|
|
856
|
-
);
|
|
857
|
-
|
|
858
|
-
if (requestGeneration !== generation.current) return;
|
|
859
|
-
setRows(response.items ?? []);
|
|
860
|
-
}
|
|
861
|
-
|
|
862
|
-
pp.effect(
|
|
863
|
-
() => () => clearTimeout(timer.current),
|
|
864
|
-
[]
|
|
865
|
-
);
|
|
866
|
-
</script>
|
|
867
|
-
</section>
|
|
868
|
-
```
|
|
869
|
-
|
|
870
|
-
The native input remains immediate, timer/query bookkeeping does not render, and only the authoritative rows update the list. The generation check prevents an older response from overwriting a newer search. See [fetch-data.md](./fetch-data.md#search-filters-and-request-races) for the RPC-side rules.
|
|
871
|
-
|
|
872
|
-
### Keep high-frequency state close to the control
|
|
873
|
-
|
|
874
|
-
Component boundaries define who owns a render. A keystroke-frequency value should live in the smallest component that needs to render from it.
|
|
875
|
-
|
|
876
|
-
- Split a search/toolbar from a large result grid when their update frequencies differ.
|
|
877
|
-
- Split frequently edited form state from unrelated dialogs, page shells, and providers.
|
|
878
|
-
- Keep shared state in a parent only when multiple children genuinely need the same render-driving value.
|
|
879
|
-
- Do not create wrapper components that still own all high-frequency state and therefore rerender every subtree.
|
|
880
|
-
|
|
881
|
-
For a controlled input that genuinely must write state on every keystroke, keep its owner small. If an expensive derived consumer may lag by one commit, use `pp.deferredValue(...)` for that consumer:
|
|
882
|
-
|
|
883
|
-
```html
|
|
884
|
-
<section>
|
|
885
|
-
<input value="{query}" oninput="setQuery(event.target.value)" />
|
|
886
|
-
<x-large-results query="{deferredQuery}" />
|
|
887
|
-
|
|
888
|
-
<script>
|
|
889
|
-
const [query, setQuery] = pp.state("");
|
|
890
|
-
const deferredQuery = pp.deferredValue(query, "");
|
|
891
|
-
</script>
|
|
892
|
-
</section>
|
|
893
|
-
```
|
|
894
|
-
|
|
895
|
-
`pp.deferredValue` is not a substitute for correct ownership, and `pp.transition()` does not time-slice or deprioritize work. PulsePoint remains synchronous.
|
|
896
|
-
|
|
897
|
-
### Avoid duplicate and invisible commits
|
|
898
|
-
|
|
899
|
-
Each setter that changes a value requests a render, even when the eventual UI change is small.
|
|
900
|
-
|
|
901
|
-
- Do not set `loading=true` for a background refresh when existing rows remain visible and no loading indicator changes.
|
|
902
|
-
- Batch related state changes in the same synchronous turn when possible; PulsePoint coalesces scheduled updates.
|
|
903
|
-
- Do not mirror the same value in multiple state hooks.
|
|
904
|
-
- Keep pagination/request cursors in refs when moving the cursor should only start work, then put returned rows and visible metadata in state.
|
|
905
|
-
- Memoize only genuinely expensive derivations or values whose identity matters. `pp.memo` does not make an unnecessary owner render free.
|
|
906
|
-
- Use keyed `pp-for` rows so reconciliation can preserve row identity.
|
|
907
|
-
|
|
908
|
-
###
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
774
|
+
## High-performance authoring
|
|
775
|
+
|
|
776
|
+
PulsePoint renders synchronously when a component requests an update. That makes state ownership and component boundaries part of the performance contract: the runtime can optimize reconciliation, but it cannot decide that an authored state change was unnecessary.
|
|
777
|
+
|
|
778
|
+
### State means "render required"
|
|
779
|
+
|
|
780
|
+
Use `pp.state(...)` when changing the value must affect at least one render-facing behavior:
|
|
781
|
+
|
|
782
|
+
- template text, attributes, lists, or conditional visibility
|
|
783
|
+
- a bound child prop or provided context value
|
|
784
|
+
- a memo, effect, or other render dependency that must follow the value
|
|
785
|
+
- authoritative data returned from an RPC call
|
|
786
|
+
|
|
787
|
+
Use `pp.ref(...)` when a mutable value must persist but changing it should not render:
|
|
788
|
+
|
|
789
|
+
- debounce and timeout handles
|
|
790
|
+
- request generations or stale-response tokens
|
|
791
|
+
- pagination cursors that are not displayed
|
|
792
|
+
- previous values and imperative integration handles
|
|
793
|
+
- transient search text used only to build a later RPC payload
|
|
794
|
+
|
|
795
|
+
A ref mutation is intentionally invisible to the template. Do not replace state with a ref when the UI is expected to update.
|
|
796
|
+
|
|
797
|
+
### Debounce frequency and render cost are different
|
|
798
|
+
|
|
799
|
+
A debounce answers "how often should this work start?" It does not answer "how much work happens when it starts?"
|
|
800
|
+
|
|
801
|
+
This shape can still stall a large owner:
|
|
802
|
+
|
|
803
|
+
```html
|
|
804
|
+
<script>
|
|
805
|
+
const [query, setQuery] = pp.state("");
|
|
806
|
+
|
|
807
|
+
function onSearch(value) {
|
|
808
|
+
clearTimeout(onSearch.timer);
|
|
809
|
+
onSearch.timer = setTimeout(() => setQuery(value), 300);
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
pp.effect(() => {
|
|
813
|
+
loadRows(query);
|
|
814
|
+
}, [query]);
|
|
815
|
+
</script>
|
|
816
|
+
```
|
|
817
|
+
|
|
818
|
+
When the timer fires, `setQuery` asks the whole owning component to render even if `query` never appears in its markup. If that owner also contains a large list, dialogs, forms, providers, or many nested components, typing that resumes near the timer can compete with the render.
|
|
819
|
+
|
|
820
|
+
When query text exists only for an RPC request, keep the query and timer in refs, debounce the request itself, and render only the accepted result:
|
|
821
|
+
|
|
822
|
+
```html
|
|
823
|
+
<section>
|
|
824
|
+
<input
|
|
825
|
+
type="search"
|
|
826
|
+
placeholder="Search products"
|
|
827
|
+
oninput="scheduleSearch(event.target.value)"
|
|
828
|
+
/>
|
|
829
|
+
|
|
830
|
+
<template pp-for="row in rows">
|
|
831
|
+
<article key="{row.id}">{row.label}</article>
|
|
832
|
+
</template>
|
|
833
|
+
|
|
834
|
+
<script>
|
|
835
|
+
const [rows, setRows] = pp.state([]);
|
|
836
|
+
const query = pp.ref("");
|
|
837
|
+
const timer = pp.ref(null);
|
|
838
|
+
const generation = pp.ref(0);
|
|
839
|
+
|
|
840
|
+
function scheduleSearch(value) {
|
|
841
|
+
query.current = value;
|
|
842
|
+
const requestGeneration = ++generation.current;
|
|
843
|
+
|
|
844
|
+
clearTimeout(timer.current);
|
|
845
|
+
timer.current = setTimeout(
|
|
846
|
+
() => search(requestGeneration),
|
|
847
|
+
300
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
async function search(requestGeneration) {
|
|
852
|
+
const response = await pp.rpc(
|
|
853
|
+
"search_products",
|
|
854
|
+
{ query: query.current },
|
|
855
|
+
{ abortPrevious: true }
|
|
856
|
+
);
|
|
857
|
+
|
|
858
|
+
if (requestGeneration !== generation.current) return;
|
|
859
|
+
setRows(response.items ?? []);
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
pp.effect(
|
|
863
|
+
() => () => clearTimeout(timer.current),
|
|
864
|
+
[]
|
|
865
|
+
);
|
|
866
|
+
</script>
|
|
867
|
+
</section>
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
The native input remains immediate, timer/query bookkeeping does not render, and only the authoritative rows update the list. The generation check prevents an older response from overwriting a newer search. See [fetch-data.md](./fetch-data.md#search-filters-and-request-races) for the RPC-side rules.
|
|
871
|
+
|
|
872
|
+
### Keep high-frequency state close to the control
|
|
873
|
+
|
|
874
|
+
Component boundaries define who owns a render. A keystroke-frequency value should live in the smallest component that needs to render from it.
|
|
875
|
+
|
|
876
|
+
- Split a search/toolbar from a large result grid when their update frequencies differ.
|
|
877
|
+
- Split frequently edited form state from unrelated dialogs, page shells, and providers.
|
|
878
|
+
- Keep shared state in a parent only when multiple children genuinely need the same render-driving value.
|
|
879
|
+
- Do not create wrapper components that still own all high-frequency state and therefore rerender every subtree.
|
|
880
|
+
|
|
881
|
+
For a controlled input that genuinely must write state on every keystroke, keep its owner small. If an expensive derived consumer may lag by one commit, use `pp.deferredValue(...)` for that consumer:
|
|
882
|
+
|
|
883
|
+
```html
|
|
884
|
+
<section>
|
|
885
|
+
<input value="{query}" oninput="setQuery(event.target.value)" />
|
|
886
|
+
<x-large-results query="{deferredQuery}" />
|
|
887
|
+
|
|
888
|
+
<script>
|
|
889
|
+
const [query, setQuery] = pp.state("");
|
|
890
|
+
const deferredQuery = pp.deferredValue(query, "");
|
|
891
|
+
</script>
|
|
892
|
+
</section>
|
|
893
|
+
```
|
|
894
|
+
|
|
895
|
+
`pp.deferredValue` is not a substitute for correct ownership, and `pp.transition()` does not time-slice or deprioritize work. PulsePoint remains synchronous.
|
|
896
|
+
|
|
897
|
+
### Avoid duplicate and invisible commits
|
|
898
|
+
|
|
899
|
+
Each setter that changes a value requests a render, even when the eventual UI change is small.
|
|
900
|
+
|
|
901
|
+
- Do not set `loading=true` for a background refresh when existing rows remain visible and no loading indicator changes.
|
|
902
|
+
- Batch related state changes in the same synchronous turn when possible; PulsePoint coalesces scheduled updates.
|
|
903
|
+
- Do not mirror the same value in multiple state hooks.
|
|
904
|
+
- Keep pagination/request cursors in refs when moving the cursor should only start work, then put returned rows and visible metadata in state.
|
|
905
|
+
- Memoize only genuinely expensive derivations or values whose identity matters. `pp.memo` does not make an unnecessary owner render free.
|
|
906
|
+
- Use keyed `pp-for` rows so reconciliation can preserve row identity.
|
|
907
|
+
|
|
908
|
+
### Keyed rows are reconciled per row, not per list
|
|
909
|
+
|
|
910
|
+
A render rebuilds the whole component as one HTML string and reconciles it against the live DOM. For a list, that used to mean changing one row cost the same as rebuilding every row: the entire list was re-serialized, re-parsed and re-walked.
|
|
911
|
+
|
|
912
|
+
The runtime now remembers the markup each keyed row produced. A row that renders byte-identically again is not re-parsed and not re-diffed — its live node is kept, along with its attributes, text and bound handlers. Reconciliation work scales with the number of rows that actually changed.
|
|
913
|
+
|
|
914
|
+
This is automatic, but it only engages for rows the runtime can safely stand in for. A row participates when:
|
|
915
|
+
|
|
916
|
+
- the loop body renders **exactly one root element** per row,
|
|
917
|
+
- that element carries a non-empty, unique `key`,
|
|
918
|
+
- the loop is not nested inside another `pp-for`, and
|
|
919
|
+
- the row's markup contains no nested component boundary, owned slot content, or context provider.
|
|
920
|
+
|
|
921
|
+
Practical consequences when authoring a large list:
|
|
922
|
+
|
|
923
|
+
- **Always key rows.** An unkeyed row can never be reused, so an unkeyed list still reconciles in full.
|
|
924
|
+
- **Keep the row body single-rooted.** A loop body with two sibling elements, or with bare text next to the element, opts the whole loop out. Wrap the row in one element.
|
|
925
|
+
- **A row built entirely from a child component** (`<template pp-for="row in rows"><x-row key="{row.id}" … /></template>`) is reconciled the normal way, because the parent is responsible for refreshing that boundary's bound props. Prefer plain markup in the row when the list is large and its rows rarely change.
|
|
926
|
+
- Keep values that feed a row stable. A row whose markup is regenerated identically is free; a row whose markup differs by even one character is full work.
|
|
927
|
+
|
|
928
|
+
### Diagnose the owner before the runtime
|
|
929
|
+
|
|
930
|
+
Classify the symptom before editing `public/js/pp-reactive-v2.js`:
|
|
931
|
+
|
|
932
|
+
| Evidence | Likely classification | First action |
|
|
933
|
+
| --- | --- | --- |
|
|
934
|
+
| A state setter fires, but the changed value is not rendered | Component authoring issue | Move non-rendering bookkeeping to a ref or call the action directly |
|
|
935
|
+
| A debounce fires just before typing stalls | Component/request ownership issue | Debounce the RPC or expensive action, not an unnecessary broad-owner setter |
|
|
936
|
+
| Loading and result setters produce two large commits | Component authoring issue | Remove invisible loading commits or isolate the visible indicator |
|
|
937
|
+
| A high-frequency control owns a large list/provider/dialog subtree | Component-boundary issue | Move the state to a smaller focused component |
|
|
938
|
+
| Rendered HTML is byte-identical but a large stable subtree is still reconciled | Possible runtime issue | Profile runtime phases and verify the stable-DOM case |
|
|
939
|
+
| One small necessary binding change spends disproportionate time in DOM diffing or nested bootstrap | Possible runtime issue | Build a focused reproduction and benchmark before changing reconciliation |
|
|
940
|
+
| Changing one row of a large list costs about as much as changing every row | Component authoring issue first | Check the row is keyed, single-rooted, and not a component boundary, so per-row reuse can engage |
|
|
941
|
+
|
|
942
|
+
Enable measurements only while profiling:
|
|
943
|
+
|
|
944
|
+
```html
|
|
945
|
+
<script>
|
|
946
|
+
pp.enablePerf();
|
|
947
|
+
pp.resetPerfStats();
|
|
948
|
+
|
|
949
|
+
// Reproduce one interaction, then inspect:
|
|
950
|
+
console.log(pp.getPerfStats());
|
|
951
|
+
|
|
952
|
+
pp.disablePerf();
|
|
953
|
+
</script>
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
Correlate the expensive component and phase with the setter or RPC completion that requested it. Compare the same interaction, DOM size, data, and number of repetitions before and after a change. Input paint timing and RPC latency are separate from render timing, so measure them separately.
|
|
957
|
+
|
|
958
|
+
Do not "fix" a PulsePoint performance problem with `querySelector`, manual listeners, `innerHTML`, or parallel DOM state. Those bypass ownership instead of correcting it.
|
|
959
|
+
|
|
960
|
+
## Context
|
|
940
961
|
|
|
941
962
|
Context is implemented in the current runtime with a React-style provider pattern rather than a legacy `pp.provideContext(...)` helper. Because Caspian templates are HTML-first, authored provider tags should be written in lowercase HTML form, for example `<themecontext.provider>`, even when the JavaScript token is named `ThemeContext`.
|
|
942
963
|
|
|
@@ -1318,12 +1339,12 @@ Use these rules when generating or editing PulsePoint runtime code:
|
|
|
1318
1339
|
- In authored Caspian templates, do not handwrite `pp-component` or `type="text/pp"`; let the render pipeline inject them.
|
|
1319
1340
|
- For grouped subtrees, follow the section layout pattern in [routing.md](./routing.md), keep the shared interactive shell in the parent folder's `layout.html`, and keep route-specific PulsePoint code in each child `index.html`.
|
|
1320
1341
|
- For grouped shells with independent shell and content scrolling, put `pp-reset-scroll="true"` on the content pane rather than the whole shell when only the page content should reset between child-route navigations.
|
|
1321
|
-
- Prefer PulsePoint state and template directives over manual DOM mutation for reactive updates.
|
|
1322
|
-
- Treat `pp.state(...)` as a request to render. Keep timers, request generations, undisplayed pagination cursors, and transient RPC-only query text in `pp.ref(...)`; do not move a value to a ref when markup or render dependencies must react to it.
|
|
1323
|
-
- A debounce limits frequency but does not reduce the cost of the update it eventually runs. For server search, debounce the RPC, discard stale responses, and keep only accepted results in state instead of waking a page-sized owner through an undisplayed query state.
|
|
1324
|
-
- Keep high-frequency input state in the smallest useful component boundary. Use `pp.deferredValue(...)` only when an expensive consumer may safely lag one commit, and remember that `pp.transition()` is not concurrent scheduling.
|
|
1325
|
-
- Before changing runtime reconciliation, use `pp.enablePerf()` to decide whether the app requested a redundant broad render or stable byte-identical output still incurred disproportionate runtime work.
|
|
1326
|
-
- Avoid generating ids, `data-*` state, `querySelector`, `getElementById`, `addEventListener`, manual `innerHTML`, or custom event buses for normal Caspian UI behavior.
|
|
1342
|
+
- Prefer PulsePoint state and template directives over manual DOM mutation for reactive updates.
|
|
1343
|
+
- Treat `pp.state(...)` as a request to render. Keep timers, request generations, undisplayed pagination cursors, and transient RPC-only query text in `pp.ref(...)`; do not move a value to a ref when markup or render dependencies must react to it.
|
|
1344
|
+
- A debounce limits frequency but does not reduce the cost of the update it eventually runs. For server search, debounce the RPC, discard stale responses, and keep only accepted results in state instead of waking a page-sized owner through an undisplayed query state.
|
|
1345
|
+
- Keep high-frequency input state in the smallest useful component boundary. Use `pp.deferredValue(...)` only when an expensive consumer may safely lag one commit, and remember that `pp.transition()` is not concurrent scheduling.
|
|
1346
|
+
- Before changing runtime reconciliation, use `pp.enablePerf()` to decide whether the app requested a redundant broad render or stable byte-identical output still incurred disproportionate runtime work.
|
|
1347
|
+
- Avoid generating ids, `data-*` state, `querySelector`, `getElementById`, `addEventListener`, manual `innerHTML`, or custom event buses for normal Caspian UI behavior.
|
|
1327
1348
|
- Avoid data-attribute click wiring such as `data-action="save"` plus a delegated listener. Use `onclick="save()"` or `onsubmit="{save(event)}"` in authored HTML and keep reactive state in `pp.state(...)`.
|
|
1328
1349
|
- If you are explicitly editing raw runtime HTML or internals, keep `pp-component` unique per live instance.
|
|
1329
1350
|
- In authored templates, use a plain `<script>` inside the root. In runtime HTML, the owned script appears as `script[type="text/pp"]`.
|
|
@@ -1386,9 +1407,9 @@ These are current runtime caveats that matter for authors and AI tools:
|
|
|
1386
1407
|
- `pp.provideContext` is not part of the current runtime API. Use an HTML-first lowercase provider tag such as `<themecontext.provider>`.
|
|
1387
1408
|
- `pp.portal()` preserves logical ancestry through the registry, so context and prop refresh behavior continue to work through portaled descendants.
|
|
1388
1409
|
- Top-level array destructuring is exported to template scope for any initializer, not only `pp.state` and `pp.reducer`. A binding declared inside a block or function is still not exported.
|
|
1389
|
-
- `pp.transition()` does not deprioritize work. PulsePoint renders synchronously; the hook provides an accurate `isPending` flag, not a concurrent scheduler.
|
|
1390
|
-
- `pp.state(...)` changes schedule a component render; `pp.ref(...)` mutations do not. This is an ownership contract, not merely a syntax choice.
|
|
1391
|
-
- Debouncing a state setter does not make the resulting render cheaper. A large owner should not hold RPC-only query text, timer handles, request generations, or undisplayed pagination cursors in state.
|
|
1410
|
+
- `pp.transition()` does not deprioritize work. PulsePoint renders synchronously; the hook provides an accurate `isPending` flag, not a concurrent scheduler.
|
|
1411
|
+
- `pp.state(...)` changes schedule a component render; `pp.ref(...)` mutations do not. This is an ownership contract, not merely a syntax choice.
|
|
1412
|
+
- Debouncing a state setter does not make the resulting render cheaper. A large owner should not hold RPC-only query text, timer handles, request generations, or undisplayed pagination cursors in state.
|
|
1392
1413
|
- `pp.syncExternalStore(...)` resubscribes whenever `subscribe` changes identity, so pass a `pp.callback(..., [])`-wrapped function.
|
|
1393
1414
|
- `pp.optimistic(...)` clears its pending actions on the render where `passthrough` changes, using a render-phase update rather than an effect, so there is no frame showing stale optimistic output.
|
|
1394
1415
|
- `pp.errorBoundary()` catches render, effect, and effect-cleanup throws, including the boundary component's own. It does not catch event-handler errors. It latches until `reset()` and stops catching after five captures without a reset.
|