@lazily-hub/lazily-js 0.21.0 → 0.24.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/.size-limit.json +44 -0
- package/BENCHMARKS.md +32 -29
- package/README.md +46 -5
- package/bench/context.bench.mjs +33 -1
- package/package.json +9 -3
- package/src/collections.js +24 -5
- package/src/presence.js +6 -1
- package/src/reactive-async.js +109 -54
- package/src/reactive.d.ts +46 -2
- package/src/reactive.js +530 -302
package/.size-limit.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"name": "reactive: Context",
|
|
4
|
+
"path": "src/reactive.js",
|
|
5
|
+
"import": "{ Context }",
|
|
6
|
+
"limit": "2239 B"
|
|
7
|
+
},
|
|
8
|
+
{
|
|
9
|
+
"name": "reactive: Context + handles + defaultEqual",
|
|
10
|
+
"path": "src/reactive.js",
|
|
11
|
+
"import": "{ Context, CellHandle, SlotHandle, SignalHandle, EffectHandle, defaultEqual }",
|
|
12
|
+
"limit": "2246 B"
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
"name": "state-machine: StateMachine",
|
|
16
|
+
"path": "src/state-machine.js",
|
|
17
|
+
"import": "{ StateMachine }",
|
|
18
|
+
"limit": "267 B"
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "sem-tree: SemTree",
|
|
22
|
+
"path": "src/sem-tree.js",
|
|
23
|
+
"import": "{ SemTree }",
|
|
24
|
+
"limit": "505 B"
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"name": "stable-id: contentHash",
|
|
28
|
+
"path": "src/stable-id.js",
|
|
29
|
+
"import": "{ contentHash }",
|
|
30
|
+
"limit": "152 B"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"name": "collections: CellMap + CellTree + reconcileCollections",
|
|
34
|
+
"path": "src/collections.js",
|
|
35
|
+
"import": "{ CellMap, CellTree, reconcileCollections }",
|
|
36
|
+
"limit": "1645 B"
|
|
37
|
+
},
|
|
38
|
+
{
|
|
39
|
+
"name": "index: PROTOCOL_ID + Snapshot (tree-shaken kitchen sink)",
|
|
40
|
+
"path": "src/index.js",
|
|
41
|
+
"import": "{ PROTOCOL_ID, Snapshot }",
|
|
42
|
+
"limit": "2409 B"
|
|
43
|
+
}
|
|
44
|
+
]
|
package/BENCHMARKS.md
CHANGED
|
@@ -19,7 +19,7 @@ like-for-like single-process counterpart here and are intentionally omitted.
|
|
|
19
19
|
|
|
20
20
|
<!-- benchmark-results:start -->
|
|
21
21
|
|
|
22
|
-
Generated for package `@lazily-hub/lazily-js` version `0.
|
|
22
|
+
Generated for package `@lazily-hub/lazily-js` version `0.22.0`.
|
|
23
23
|
|
|
24
24
|
Environment: Node.js `26.4.0` on `linux x64`.
|
|
25
25
|
|
|
@@ -33,34 +33,37 @@ Mean wall-clock time per iteration; 95% CI half-width from the standard error.
|
|
|
33
33
|
|
|
34
34
|
| Group | Case | Mean | 95% CI | p75 | p99 | Samples |
|
|
35
35
|
|---|---|---:|---:|---:|---:|---:|
|
|
36
|
-
| cached_reads | context |
|
|
37
|
-
| cold_first_get | context |
|
|
38
|
-
| dependency_fan_out | context / 32 |
|
|
39
|
-
| dependency_fan_out | context / 256 |
|
|
40
|
-
| set_cell_invalidation | high_fan_out / 512 |
|
|
41
|
-
| memo_equality_suppression | context | 2.
|
|
42
|
-
| effect_flushing | context |
|
|
43
|
-
| batch_storms | context / 64 |
|
|
44
|
-
| typed_cache_reads | context_cell |
|
|
45
|
-
| typed_cache_reads | context_slot |
|
|
46
|
-
|
|
|
47
|
-
|
|
|
48
|
-
|
|
|
49
|
-
|
|
|
50
|
-
|
|
|
51
|
-
|
|
|
52
|
-
|
|
|
53
|
-
|
|
|
54
|
-
|
|
|
55
|
-
|
|
|
56
|
-
|
|
|
57
|
-
|
|
|
58
|
-
|
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
36
|
+
| cached_reads | context | 8.061 ns | ± 0.779 ns | 8.427 ns | 26.939 ns | 100 |
|
|
37
|
+
| cold_first_get | context | 423.300 ns | ± 43.318 ns | 342.500 ns | 1.373 us | 100 |
|
|
38
|
+
| dependency_fan_out | context / 32 | 3.771 us | ± 173.255 ns | 3.800 us | 7.261 us | 100 |
|
|
39
|
+
| dependency_fan_out | context / 256 | 47.978 us | ± 1.173 us | 47.323 us | 66.724 us | 100 |
|
|
40
|
+
| set_cell_invalidation | high_fan_out / 512 | 3.424 us | ± 84.646 ns | 3.410 us | 5.966 us | 100 |
|
|
41
|
+
| memo_equality_suppression | context | 2.783 us | ± 561.495 ns | 4.138 us | 13.663 us | 100 |
|
|
42
|
+
| effect_flushing | context | 188.019 ns | ± 8.596 ns | 191.316 ns | 376.753 ns | 100 |
|
|
43
|
+
| batch_storms | context / 64 | 14.257 us | ± 614.920 ns | 14.799 us | 26.218 us | 70 |
|
|
44
|
+
| typed_cache_reads | context_cell | 4.742 ns | ± 0.403 ns | 5.609 ns | 8.350 ns | 100 |
|
|
45
|
+
| typed_cache_reads | context_slot | 42.333 ns | ± 35.346 ns | 19.026 ns | 188.015 ns | 100 |
|
|
46
|
+
| default_equal | array | 34.602 ns | ± 2.774 ns | 30.943 ns | 99.200 ns | 100 |
|
|
47
|
+
| default_equal | object | 94.251 ns | ± 3.295 ns | 93.296 ns | 161.861 ns | 100 |
|
|
48
|
+
| node_allocation | scale / 4096 | 163.011 us | ± 8.360 us | 159.941 us | 322.151 us | 100 |
|
|
49
|
+
| textcrdt_insert_str | 64 | 9.882 us | ± 3.182 us | 8.358 us | 59.799 us | 100 |
|
|
50
|
+
| textcrdt_insert_str | 256 | 28.812 us | ± 2.264 us | 25.503 us | 65.357 us | 100 |
|
|
51
|
+
| textcrdt_insert_str | 1024 | 97.365 us | ± 5.110 us | 91.328 us | 166.233 us | 100 |
|
|
52
|
+
| textcrdt_repeated_text | 64 | 40.114 us | ± 1.467 us | 37.831 us | 64.145 us | 100 |
|
|
53
|
+
| textcrdt_repeated_text | 256 | 159.171 us | ± 8.707 us | 171.325 us | 335.391 us | 100 |
|
|
54
|
+
| textcrdt_repeated_text | 1024 | 915.084 us | ± 45.457 us | 1.025 ms | 1.667 ms | 100 |
|
|
55
|
+
| textcrdt_merge | 64 | 13.957 us | ± 1.733 us | 14.396 us | 22.914 us | 100 |
|
|
56
|
+
| textcrdt_merge | 256 | 65.644 us | ± 5.379 us | 69.226 us | 130.124 us | 100 |
|
|
57
|
+
| textcrdt_merge | 1024 | 297.418 us | ± 16.007 us | 319.127 us | 535.482 us | 100 |
|
|
58
|
+
| textcrdt_delta_sync | 64 | 21.147 us | ± 2.856 us | 21.657 us | 42.016 us | 100 |
|
|
59
|
+
| textcrdt_delta_sync | 256 | 64.549 us | ± 3.040 us | 64.218 us | 151.264 us | 100 |
|
|
60
|
+
| textcrdt_delta_sync | 1024 | 252.134 us | ± 11.758 us | 248.660 us | 496.058 us | 100 |
|
|
61
|
+
| seqcrdt_insert_back | 64 | 174.152 us | ± 173.997 us | 108.671 us | 227.421 us | 100 |
|
|
62
|
+
| seqcrdt_insert_back | 256 | 1.645 ms | ± 70.745 us | 1.836 ms | 2.722 ms | 100 |
|
|
63
|
+
| seqcrdt_insert_back | 1024 | 43.273 ms | ± 2.036 ms | 44.806 ms | 55.153 ms | 20 |
|
|
64
|
+
| seqcrdt_merge | 64 | 7.911 us | ± 1.244 us | 7.920 us | 23.291 us | 100 |
|
|
65
|
+
| seqcrdt_merge | 256 | 27.352 us | ± 2.299 us | 31.073 us | 66.754 us | 100 |
|
|
66
|
+
| seqcrdt_merge | 1024 | 117.426 us | ± 8.835 us | 127.046 us | 355.789 us | 100 |
|
|
64
67
|
|
|
65
68
|
<!-- benchmark-results:end -->
|
|
66
69
|
|
package/README.md
CHANGED
|
@@ -101,7 +101,7 @@ and JSON Schemas in `lazily-spec` and the Lean models in `lazily-formal`.
|
|
|
101
101
|
|--------|------------|
|
|
102
102
|
| `@lazily-hub/lazily-js` | `lazily-spec` IPC wire types: `Snapshot`, `Delta`, `DeltaOp`, `IpcMessage` (`Snapshot` / `Delta` / `CrdtSync`), `NodeState`, `IpcValue`, `PeerPermissions`, `SessionHandshake`, `BINDING_CAPABILITIES` |
|
|
103
103
|
| `@lazily-hub/lazily-js/transport` | Cross-process zero-copy transport (`#lzzcpy`): `ShmBlobArena`, `InProcessBackend` / `ArrowBackend`, `BlobRouter`, `spillMessage` / `resolveValue`, and the FFI-gated `createShmBackend` (Node/Bun/Deno). Isomorphic — no FFI import; browser-safe |
|
|
104
|
-
| `@lazily-hub/lazily-js/reactive` | Reactive dependency graph: `Context
|
|
104
|
+
| `@lazily-hub/lazily-js/reactive` | Reactive dependency graph: `createContext` (alias `Context`), `Cell`, `Slot`, `Signal`, `Effect`. Closure-based core (#lzjsclosure) — 2-8x faster reads than the prior class implementation |
|
|
105
105
|
| `@lazily-hub/lazily-js/reactive-async` | Async reactive graph: `AsyncContext` — Promise-driven slots/effects with revision-guarded stale-completion discard, in-flight dedup, and cancellation |
|
|
106
106
|
| `@lazily-hub/lazily-js/reactive-family` | Unified keyed reactive map: `ReactiveMap<K,V,H>` (reactive membership/order, `getOrInsertWith` mint-on-access, `remove`, `move`) + `CellMap` (adds cell-only `set` + eager `entry`/`entryWith`) and `SlotMap` (lazy `getOrInsertWith` + eager `materializeAll`; no `set`) specializations. No eager/lazy mode flag (`#reactivemap`) |
|
|
107
107
|
| `@lazily-hub/lazily-js/async-reactive-family` | Async keyed reactive map: `AsyncReactiveMap` + `AsyncCellMap` / `AsyncSlotMap` over `AsyncContext` — eventual transparency (a pending slot observes `undefined` and resolves to the canonical value; eager ≡ lazy once resolved) (`#reactivemap`) |
|
|
@@ -122,8 +122,18 @@ and JSON Schemas in `lazily-spec` and the Lean models in `lazily-formal`.
|
|
|
122
122
|
|
|
123
123
|
## Reactive graph
|
|
124
124
|
|
|
125
|
-
`Context`
|
|
126
|
-
JavaScript. The
|
|
125
|
+
`Context` (alias `createContext`, from `@lazily-hub/lazily-js/reactive`) mirrors
|
|
126
|
+
the single-threaded lazily-rs `Context` semantics in native JavaScript. The
|
|
127
|
+
reactive core is implemented with the **closure factory technique** (#lzjsclosure,
|
|
128
|
+
rmemo-style): `createContext()` returns an object whose methods close over
|
|
129
|
+
captured graph state, and nodes are plain objects with a numeric discriminator
|
|
130
|
+
replacing `instanceof`. V8 inlines these small monomorphic closures more
|
|
131
|
+
aggressively than `class` + `#private` methods, so the read/invalidate hot paths
|
|
132
|
+
run 2-8x faster than the prior class implementation (see `bench/context.bench.mjs`
|
|
133
|
+
and `BENCHMARKS.md`). Both `createContext()` and the historical `new Context()`
|
|
134
|
+
are the same function — an alias, not a wrapper.
|
|
135
|
+
|
|
136
|
+
The family is:
|
|
127
137
|
|
|
128
138
|
- **Slot** - lazy, memoized derived value;
|
|
129
139
|
- **Cell** - mutable source value;
|
|
@@ -139,9 +149,9 @@ to suppress downstream work when a recompute produces the same value. `batch`
|
|
|
139
149
|
coalesces invalidations and effect reruns.
|
|
140
150
|
|
|
141
151
|
```js
|
|
142
|
-
import { Context } from "@lazily-hub/lazily-js/reactive";
|
|
152
|
+
import { Context, createContext } from "@lazily-hub/lazily-js/reactive";
|
|
143
153
|
|
|
144
|
-
const ctx = new Context()
|
|
154
|
+
const ctx = createContext(); // idiomatic; `new Context()` is the same call
|
|
145
155
|
const a = ctx.cell(2);
|
|
146
156
|
const b = ctx.cell(3);
|
|
147
157
|
|
|
@@ -650,6 +660,37 @@ npm run benchmark-update # refresh BENCHMARKS.md's generated micro-bench table
|
|
|
650
660
|
npm run benchmark-check # CI gate: exit 1 if the micro-bench row set is stale
|
|
651
661
|
```
|
|
652
662
|
|
|
663
|
+
## Bundle size
|
|
664
|
+
|
|
665
|
+
`@lazily-hub/lazily-js` ships as pure ES modules with `"sideEffects": false` and
|
|
666
|
+
one file per subpath export, so bundlers tree-shake to exactly what you import.
|
|
667
|
+
The budgets below are enforced in CI by `npm run test:size` (size-limit); the
|
|
668
|
+
table is regenerated on every `npm run build` so it cannot drift from the
|
|
669
|
+
shipped bytes.
|
|
670
|
+
|
|
671
|
+
<!-- size-limits:start -->
|
|
672
|
+
|
|
673
|
+
Generated for package `@lazily-hub/lazily-js` version `0.24.0`. Every entry is **minified + brotlied, tree-shaken to the named import** (`size-limit` + esbuild, the same pipeline Webpack/Rollup/Vite apply via `"sideEffects": false`).
|
|
674
|
+
|
|
675
|
+
Refresh command:
|
|
676
|
+
|
|
677
|
+
```bash
|
|
678
|
+
npm run build # regenerates this table as part of every build
|
|
679
|
+
npm run test:size # gate: fails CI if any entry exceeds its budget
|
|
680
|
+
```
|
|
681
|
+
|
|
682
|
+
| Import | Size | Budget |
|
|
683
|
+
|---|---:|---:|
|
|
684
|
+
| reactive: Context | 2.24 KB ✓ | 2.24 KB |
|
|
685
|
+
| reactive: Context + handles + defaultEqual | 2.24 KB ✓ | 2.25 KB |
|
|
686
|
+
| state-machine: StateMachine | 267 B ✓ | 267 B |
|
|
687
|
+
| sem-tree: SemTree | 505 B ✓ | 505 B |
|
|
688
|
+
| stable-id: contentHash | 152 B ✓ | 152 B |
|
|
689
|
+
| collections: CellMap + CellTree + reconcileCollections | 1.65 KB ✓ | 1.65 KB |
|
|
690
|
+
| index: PROTOCOL_ID + Snapshot (tree-shaken kitchen sink) | 2.41 KB ✓ | 2.41 KB |
|
|
691
|
+
|
|
692
|
+
<!-- size-limits:end -->
|
|
693
|
+
|
|
653
694
|
## See also
|
|
654
695
|
|
|
655
696
|
- [`lazily-spec`][spec] - language-agnostic wire protocol, schemas, and
|
package/bench/context.bench.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// comparable. Constants mirror `benches/context.rs` exactly.
|
|
11
11
|
|
|
12
12
|
import { bench, blackBox, run } from "./harness.mjs";
|
|
13
|
-
import { Context } from "../src/reactive.js";
|
|
13
|
+
import { Context, defaultEqual } from "../src/reactive.js";
|
|
14
14
|
|
|
15
15
|
const FAN_OUT_WIDTHS = [32, 256];
|
|
16
16
|
const MEMO_CHAIN_DEPTH = 32;
|
|
@@ -174,6 +174,38 @@ bench.batched(
|
|
|
174
174
|
bench("typed_cache_reads", "context_cell", () => blackBox(ctx.getCell(blackBox(cell))));
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
// --- default_equal: structural object comparison (#lzjsshalloweq) -----------
|
|
178
|
+
// Times the equality guard hot path on its own (object + array shapes). The
|
|
179
|
+
// inline Array.isArray fast path + index loop is the Phase 2 win.
|
|
180
|
+
{
|
|
181
|
+
const a = { id: 42, name: "alice", tags: ["x", "y", "z"], n: 7, active: true };
|
|
182
|
+
const b = { id: 42, name: "alice", tags: ["x", "y", "z"], n: 7, active: true };
|
|
183
|
+
bench("default_equal", "object", () => blackBox(defaultEqual(a, b)));
|
|
184
|
+
}
|
|
185
|
+
{
|
|
186
|
+
const a = [1, 2, 3, 4, 5, 6, 7, 8];
|
|
187
|
+
const b = [1, 2, 3, 4, 5, 6, 7, 8];
|
|
188
|
+
bench("default_equal", "array", () => blackBox(defaultEqual(a, b)));
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// --- node_allocation: node creation at scale (#lzjslazyedges) ---------------
|
|
192
|
+
// Allocates a mix of cells, memos, and effects (no edges) so the per-node
|
|
193
|
+
// constructor cost — now free of eager [] edge arrays — is what is timed.
|
|
194
|
+
// The lazy-edges win is primarily RSS; this case guards the constructor
|
|
195
|
+
// time regression and surfaces the allocation speedup.
|
|
196
|
+
const ALLOC_NODES = 4096;
|
|
197
|
+
bench.batched(
|
|
198
|
+
"node_allocation",
|
|
199
|
+
`scale / ${ALLOC_NODES}`,
|
|
200
|
+
() => ({ ctx: new Context() }),
|
|
201
|
+
({ ctx }) => {
|
|
202
|
+
for (let i = 0; i < ALLOC_NODES; i++) {
|
|
203
|
+
ctx.cell(i);
|
|
204
|
+
}
|
|
205
|
+
blackBox(ctx);
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
|
|
177
209
|
await run({
|
|
178
210
|
format: process.env.BENCH_FORMAT === "json" ? "json" : "markdown",
|
|
179
211
|
});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazily-hub/lazily-js",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.24.0",
|
|
4
4
|
"description": "Native JavaScript port of the lazily reactive core: a full reactive graph (Cell/Slot/Signal/Effect), sync/async/thread-safe keyed reactive maps (ReactiveMap/CellMap/SlotMap), the lazily-spec IPC wire types + C-ABI FFI boundary (isomorphic channel + Node native binding), keyed cell collections + LIS reconciliation, the memoized semantic tree, the move-aware sequence CRDT, the Fugue/RGA text CRDT, manufactured text identity, full-Harel state charts, an FFI state-projection consumer, and an in-library instrumentation/benchmark API.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -147,22 +147,28 @@
|
|
|
147
147
|
"default": "./src/distributed.js"
|
|
148
148
|
}
|
|
149
149
|
},
|
|
150
|
+
"sideEffects": false,
|
|
150
151
|
"scripts": {
|
|
151
|
-
"build": "node --check src/index.js && node --check src/transport.js && node --check src/shm-backend.js && node --check src/reactive.js && node --check src/reactive-async.js && node --check src/reactive-family.js && node --check src/async-reactive-family.js && node --check src/thread-safe.js && node --check src/thread-safe-reactive-family.js && node --check src/ffi.js && node --check src/ffi-node.js && node --check src/instrumentation.js && node --check src/state-machine.js && node --check src/statechart.js && node --check src/collections.js && node --check src/queue.js && node --check src/merge.js && node --check src/relay.js && node --check src/rateshape.js && node --check src/membership.js && node --check src/coordination.js && node --check src/presence.js && node --check src/windowing.js && node --check src/resilience.js && node --check src/service.js && node --check src/temporal.js && node --check src/sem-tree.js && node --check src/stable-id.js && node --check src/seq-crdt.js && node --check src/text-crdt.js && node --check src/indexeddb-outbox.js && node --check src/utf8-offsets.js && node --check src/lossless-tree-crdt.js && node --check src/state-projection.js && node --check src/graph-view.js && node --check src/signaling.js && node --check src/distributed.js",
|
|
152
|
+
"build": "node --check src/index.js && node --check src/transport.js && node --check src/shm-backend.js && node --check src/reactive.js && node --check src/reactive-async.js && node --check src/reactive-family.js && node --check src/async-reactive-family.js && node --check src/thread-safe.js && node --check src/thread-safe-reactive-family.js && node --check src/ffi.js && node --check src/ffi-node.js && node --check src/instrumentation.js && node --check src/state-machine.js && node --check src/statechart.js && node --check src/collections.js && node --check src/queue.js && node --check src/merge.js && node --check src/relay.js && node --check src/rateshape.js && node --check src/membership.js && node --check src/coordination.js && node --check src/presence.js && node --check src/windowing.js && node --check src/resilience.js && node --check src/service.js && node --check src/temporal.js && node --check src/sem-tree.js && node --check src/stable-id.js && node --check src/seq-crdt.js && node --check src/text-crdt.js && node --check src/indexeddb-outbox.js && node --check src/utf8-offsets.js && node --check src/lossless-tree-crdt.js && node --check src/state-projection.js && node --check src/graph-view.js && node --check src/signaling.js && node --check src/distributed.js && node scripts/update-size-limits.mjs",
|
|
152
153
|
"test:formal": "node scripts/formal-check.mjs",
|
|
154
|
+
"test:size": "size-limit",
|
|
155
|
+
"size:update": "node scripts/update-size-limits.mjs",
|
|
156
|
+
"size:check": "node scripts/update-size-limits.mjs --check",
|
|
153
157
|
"bench": "node bench/context.bench.mjs",
|
|
154
158
|
"bench:scale": "node --max-old-space-size=8192 bench/scale.bench.mjs",
|
|
155
159
|
"benchmark-update": "node scripts/run-benchmarks.mjs",
|
|
156
160
|
"benchmark-check": "node scripts/run-benchmarks.mjs --check",
|
|
157
|
-
"test": "npm run test:formal && node --test test/*.test.js"
|
|
161
|
+
"test": "npm run test:formal && npm run test:size && node --test test/*.test.js"
|
|
158
162
|
},
|
|
159
163
|
"dependencies": {
|
|
160
164
|
"koffi": "^3.0.2"
|
|
161
165
|
},
|
|
162
166
|
"devDependencies": {
|
|
167
|
+
"@size-limit/preset-small-lib": "^12.1.0",
|
|
163
168
|
"@types/node": "^22.0.0",
|
|
164
169
|
"ajv": "^8.20.0",
|
|
165
170
|
"fake-indexeddb": "^6.2.5",
|
|
171
|
+
"size-limit": "^12.1.0",
|
|
166
172
|
"tsx": "^4.19.0",
|
|
167
173
|
"typescript": "^5.6.0"
|
|
168
174
|
}
|
package/src/collections.js
CHANGED
|
@@ -15,13 +15,32 @@ function deepEqual(a, b) {
|
|
|
15
15
|
if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
|
|
16
16
|
return false;
|
|
17
17
|
}
|
|
18
|
+
// #lzjsshalloweq: fast path for plain arrays — Array.isArray + length check
|
|
19
|
+
// before Object.keys, index loop, no closure allocation.
|
|
20
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
21
|
+
const n = a.length;
|
|
22
|
+
if (n !== b.length) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
for (let i = 0; i < n; i++) {
|
|
26
|
+
if (!deepEqual(a[i], b[i])) {
|
|
27
|
+
return false;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return true;
|
|
31
|
+
}
|
|
18
32
|
const aKeys = Object.keys(a);
|
|
19
33
|
const bKeys = Object.keys(b);
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
34
|
+
if (aKeys.length !== bKeys.length) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
for (let i = 0; i < aKeys.length; i++) {
|
|
38
|
+
const k = aKeys[i];
|
|
39
|
+
if (!Object.is(aKeys[k], bKeys[k]) || !deepEqual(a[k], b[k])) {
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
return true;
|
|
25
44
|
}
|
|
26
45
|
|
|
27
46
|
function indexOfKey(order, key) {
|
package/src/presence.js
CHANGED
|
@@ -65,10 +65,15 @@ export class EphemeralCell {
|
|
|
65
65
|
// ---------------------------------------------------------------------------
|
|
66
66
|
|
|
67
67
|
function mapEquals(a, b) {
|
|
68
|
+
// #lzjsshalloweq: shallow key->value equality with an index loop (no
|
|
69
|
+
// closure). Short-circuits on the first mismatched value.
|
|
68
70
|
const ak = Object.keys(a);
|
|
69
71
|
const bk = Object.keys(b);
|
|
70
72
|
if (ak.length !== bk.length) return false;
|
|
71
|
-
for (
|
|
73
|
+
for (let i = 0; i < ak.length; i++) {
|
|
74
|
+
const k = ak[i];
|
|
75
|
+
if (a[k] !== b[k]) return false;
|
|
76
|
+
}
|
|
72
77
|
return true;
|
|
73
78
|
}
|
|
74
79
|
|