@shapeshift-labs/frontier-engine 0.0.1 → 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/README.md +179 -29
- package/benchmarks/package-bench.mjs +71 -0
- package/dist/engine.d.ts +3 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +3843 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +3 -0
- package/dist/index.js.map +1 -0
- package/dist/profile.d.ts +25 -0
- package/dist/profile.d.ts.map +1 -0
- package/dist/profile.js +268 -0
- package/dist/profile.js.map +1 -0
- package/dist/types.d.ts +121 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +41 -14
- package/index.d.ts +0 -1
- package/index.js +0 -1
package/README.md
CHANGED
|
@@ -1,51 +1,201 @@
|
|
|
1
1
|
# Frontier Engine
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Stateful planned diff engine, adaptive profiles, history planning, and reusable diff caches for Frontier.
|
|
4
4
|
|
|
5
|
-
This package
|
|
5
|
+
This package sits above [`@shapeshift-labs/frontier`](https://www.npmjs.com/package/@shapeshift-labs/frontier), the small JSON diff/apply core package. It uses [`@shapeshift-labs/frontier-codec`](https://www.npmjs.com/package/@shapeshift-labs/frontier-codec) for patch-history byte helpers. Keeping the engine separate keeps core imports small while giving state, history, and CRDT layers a shared planning surface.
|
|
6
6
|
|
|
7
7
|
- npm: [`@shapeshift-labs/frontier-engine`](https://www.npmjs.com/package/@shapeshift-labs/frontier-engine)
|
|
8
8
|
- source: [`siliconjungle/-shapeshift-labs-frontier-engine`](https://github.com/siliconjungle/-shapeshift-labs-frontier-engine)
|
|
9
|
-
- core package: [`@shapeshift-labs/frontier`](https://www.npmjs.com/package/@shapeshift-labs/frontier)
|
|
10
|
-
- codec package: [`@shapeshift-labs/frontier-codec`](https://www.npmjs.com/package/@shapeshift-labs/frontier-codec)
|
|
11
9
|
- license: MIT
|
|
12
10
|
|
|
13
|
-
##
|
|
11
|
+
## Related Packages
|
|
12
|
+
|
|
13
|
+
- [`@shapeshift-labs/frontier`](https://www.npmjs.com/package/@shapeshift-labs/frontier): core JSON diff/apply primitives.
|
|
14
|
+
- [`@shapeshift-labs/frontier-codec`](https://www.npmjs.com/package/@shapeshift-labs/frontier-codec): patch serialization, binary frames, canonical JSON, and patch-history codecs.
|
|
15
|
+
- [`@shapeshift-labs/frontier-query`](https://www.npmjs.com/package/@shapeshift-labs/frontier-query): shared query-key, selector path, condition, identity, and table-schema primitives.
|
|
16
|
+
- [`@shapeshift-labs/frontier-mutation`](https://www.npmjs.com/package/@shapeshift-labs/frontier-mutation): explicit mutation and selector plans that can use engine-backed diff planning.
|
|
17
|
+
|
|
18
|
+
Package source repositories:
|
|
19
|
+
|
|
20
|
+
- [`siliconjungle/-shapeshift-labs-frontier`](https://github.com/siliconjungle/-shapeshift-labs-frontier)
|
|
21
|
+
- [`siliconjungle/-shapeshift-labs-frontier-codec`](https://github.com/siliconjungle/-shapeshift-labs-frontier-codec)
|
|
22
|
+
- [`siliconjungle/-shapeshift-labs-frontier-query`](https://github.com/siliconjungle/-shapeshift-labs-frontier-query)
|
|
23
|
+
- [`siliconjungle/-shapeshift-labs-frontier-mutation`](https://github.com/siliconjungle/-shapeshift-labs-frontier-mutation)
|
|
24
|
+
|
|
25
|
+
## Install
|
|
26
|
+
|
|
27
|
+
```sh
|
|
28
|
+
npm install @shapeshift-labs/frontier @shapeshift-labs/frontier-codec @shapeshift-labs/frontier-engine
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```ts
|
|
34
|
+
import { applyPatchImmutable } from '@shapeshift-labs/frontier';
|
|
35
|
+
import { createDiffEngine } from '@shapeshift-labs/frontier-engine';
|
|
36
|
+
|
|
37
|
+
const engine = createDiffEngine({
|
|
38
|
+
schema: {
|
|
39
|
+
type: 'array',
|
|
40
|
+
path: ['todos'],
|
|
41
|
+
key: 'id',
|
|
42
|
+
item: {
|
|
43
|
+
type: 'object',
|
|
44
|
+
fields: ['id', 'done', 'title']
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const before = {
|
|
50
|
+
todos: [{ id: 'a', done: false, title: 'Draft' }]
|
|
51
|
+
};
|
|
52
|
+
const after = {
|
|
53
|
+
todos: [{ id: 'a', done: true, title: 'Draft' }]
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
const patch = engine.diff(before, after);
|
|
57
|
+
const next = applyPatchImmutable(before, patch);
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## API
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
import {
|
|
64
|
+
createDiffEngine,
|
|
65
|
+
cloneProfilePlans,
|
|
66
|
+
createEngineProfilePlansSnapshot,
|
|
67
|
+
mergeProfilePlans,
|
|
68
|
+
readProfilePlans,
|
|
69
|
+
type DiffEngine,
|
|
70
|
+
type DiffProfile,
|
|
71
|
+
type EngineOptions,
|
|
72
|
+
type ProfilePlans
|
|
73
|
+
} from '@shapeshift-labs/frontier-engine';
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### `createDiffEngine(options?)`
|
|
77
|
+
|
|
78
|
+
Creates a reusable diff engine with optional schema, adaptive learning, history planning, and equality/profile helpers.
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
const engine = createDiffEngine({
|
|
82
|
+
adaptive: true,
|
|
83
|
+
adaptiveThreshold: 2,
|
|
84
|
+
arrayKey: 'id'
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
const patch = engine.diff(before, after);
|
|
88
|
+
const reusable = [];
|
|
89
|
+
engine.diffInto(before, after, reusable);
|
|
90
|
+
```
|
|
14
91
|
|
|
15
|
-
|
|
92
|
+
### Schema Plans
|
|
93
|
+
|
|
94
|
+
Schema plans are trusted shape hints for hot JSON structures. They let the engine skip generic discovery and emit compact patches for common record-array and object shapes.
|
|
95
|
+
|
|
96
|
+
```ts
|
|
97
|
+
const engine = createDiffEngine({
|
|
98
|
+
schema: {
|
|
99
|
+
type: 'array',
|
|
100
|
+
path: ['rows'],
|
|
101
|
+
key: 'id',
|
|
102
|
+
item: {
|
|
103
|
+
type: 'object',
|
|
104
|
+
fields: ['id', 'score', 'active', 'label']
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Adaptive Profiles
|
|
111
|
+
|
|
112
|
+
Adaptive engines can learn a profile from representative before/after pairs and replay that profile later.
|
|
113
|
+
|
|
114
|
+
```ts
|
|
115
|
+
const trainer = createDiffEngine({ adaptive: true });
|
|
116
|
+
const profile = trainer.train([[before, after]]);
|
|
117
|
+
|
|
118
|
+
const profiled = createDiffEngine({ profile });
|
|
119
|
+
const patch = profiled.diff(before, after);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
### History Helpers
|
|
123
|
+
|
|
124
|
+
The engine can plan patch histories, then delegate binary history encoding to `@shapeshift-labs/frontier-codec`.
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
const patches = engine.diffHistory(initial, states);
|
|
128
|
+
const bytes = engine.encodeHistory(patches);
|
|
129
|
+
const final = engine.applyEncodedHistory(initial, bytes);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
### Profile Plan Helpers
|
|
133
|
+
|
|
134
|
+
```ts
|
|
135
|
+
const plans = createEngineProfilePlansSnapshot(undefined, {
|
|
136
|
+
schemaCount: 1,
|
|
137
|
+
adaptivePlan: false,
|
|
138
|
+
historyStrategy: 'auto'
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
const merged = mergeProfilePlans(plans, { codec: { history: 'binary' } });
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
## Subpath Imports
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
import { createDiffEngine } from '@shapeshift-labs/frontier-engine/engine';
|
|
148
|
+
import { mergeProfilePlans } from '@shapeshift-labs/frontier-engine/profile';
|
|
149
|
+
import type { DiffEngine } from '@shapeshift-labs/frontier-engine/types';
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
## Package Scope
|
|
16
153
|
|
|
17
|
-
|
|
18
|
-
- adaptive shape learning and schema/profile planning;
|
|
19
|
-
- reusable engine caches and failed-plan thresholds;
|
|
20
|
-
- profile plan snapshots shared by state, history, codec, and CRDT layers;
|
|
21
|
-
- engine-level history helpers that delegate byte formats to `frontier-codec`.
|
|
154
|
+
This package owns:
|
|
22
155
|
|
|
23
|
-
|
|
156
|
+
- `createDiffEngine()`.
|
|
157
|
+
- Adaptive shape learning.
|
|
158
|
+
- Explicit schema/profile diff planning.
|
|
159
|
+
- Reusable engine caches.
|
|
160
|
+
- Profile plan snapshots shared by state/history/codec/CRDT layers.
|
|
161
|
+
- Engine-level history helpers that delegate byte formats to `frontier-codec`.
|
|
162
|
+
|
|
163
|
+
It does not own:
|
|
24
164
|
|
|
25
|
-
|
|
165
|
+
- Stateless diff/apply primitives. Those stay in `@shapeshift-labs/frontier`.
|
|
166
|
+
- Patch wire formats. Those stay in `@shapeshift-labs/frontier-codec`.
|
|
167
|
+
- State subscriptions, routers, or maintained views. Those stay in Frontier state packages.
|
|
168
|
+
- CRDT actors, updates, heads, branches, sync, awareness, rich text, or providers.
|
|
26
169
|
|
|
27
|
-
|
|
170
|
+
## Validation
|
|
28
171
|
|
|
29
|
-
|
|
172
|
+
```sh
|
|
173
|
+
npm test
|
|
174
|
+
npm run fuzz
|
|
175
|
+
npm run bench
|
|
176
|
+
npm run pack:dry
|
|
177
|
+
```
|
|
30
178
|
|
|
31
|
-
|
|
179
|
+
The package test suite covers root and subpath imports, schema diff/apply replay, profile snapshots, history planning, encoded history replay, and the absence of state/CRDT exports. The fuzzer covers schema and adaptive profile round-trips over record-array and object-shaped JSON.
|
|
32
180
|
|
|
33
|
-
|
|
181
|
+
## Benchmarks
|
|
34
182
|
|
|
35
|
-
|
|
36
|
-
- [`@shapeshift-labs/frontier-codec`](https://www.npmjs.com/package/@shapeshift-labs/frontier-codec)
|
|
37
|
-
- [`@shapeshift-labs/frontier-mutation`](https://www.npmjs.com/package/@shapeshift-labs/frontier-mutation)
|
|
183
|
+
Run the package-local benchmark:
|
|
38
184
|
|
|
39
|
-
|
|
185
|
+
```sh
|
|
186
|
+
npm run bench
|
|
187
|
+
```
|
|
40
188
|
|
|
41
|
-
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
-
|
|
48
|
-
|
|
189
|
+
Latest local package-gate run on Node v26.1.0, darwin arm64, 3 rounds:
|
|
190
|
+
|
|
191
|
+
| Fixture | Median | p95 |
|
|
192
|
+
| --- | ---: | ---: |
|
|
193
|
+
| Engine schema diff, 1k rows | 16.67 us | 17.45 us |
|
|
194
|
+
| Engine apply via core patch | 0.58 us | 0.59 us |
|
|
195
|
+
| Engine equality no-op | 9.32 us | 9.44 us |
|
|
196
|
+
| Engine history encode/decode/apply | 3.97 us | 4.22 us |
|
|
197
|
+
|
|
198
|
+
These are Frontier-only package measurements, not competitor comparisons.
|
|
49
199
|
|
|
50
200
|
## License
|
|
51
201
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { performance } from 'node:perf_hooks';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
|
|
6
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const rootDir = path.resolve(__dirname, '..');
|
|
8
|
+
const args = parseArgs(process.argv.slice(2));
|
|
9
|
+
const rounds = readPositiveInt(args.rounds, 9);
|
|
10
|
+
const outPath = args.out ? path.resolve(rootDir, args.out) : null;
|
|
11
|
+
let sink = 0;
|
|
12
|
+
|
|
13
|
+
function measure(fn, inner) {
|
|
14
|
+
for (let i = 0; i < inner; i++) fn();
|
|
15
|
+
const samples = new Array(rounds);
|
|
16
|
+
for (let roundIndex = 0; roundIndex < rounds; roundIndex++) {
|
|
17
|
+
const start = performance.now();
|
|
18
|
+
for (let i = 0; i < inner; i++) fn();
|
|
19
|
+
samples[roundIndex] = ((performance.now() - start) * 1000) / inner;
|
|
20
|
+
}
|
|
21
|
+
samples.sort((left, right) => left - right);
|
|
22
|
+
return { median: percentile(samples, 0.5), p95: percentile(samples, 0.95) };
|
|
23
|
+
}
|
|
24
|
+
function runRow(name, inner, fn, extra = {}) {
|
|
25
|
+
const timing = measure(fn, inner);
|
|
26
|
+
return { fixture: name, medianUs: round(timing.median), p95Us: round(timing.p95), ...extra };
|
|
27
|
+
}
|
|
28
|
+
function printReport(report) {
|
|
29
|
+
console.log(report.package + ' package benchmark');
|
|
30
|
+
console.log('Node ' + report.node + ' on ' + report.platform + ', rounds=' + rounds);
|
|
31
|
+
console.log('These are Frontier-only package measurements, not competitor comparisons.');
|
|
32
|
+
console.log('');
|
|
33
|
+
console.log(padRight('Fixture', 44) + padLeft('Median', 12) + padLeft('p95', 11));
|
|
34
|
+
for (const row of report.rows) {
|
|
35
|
+
console.log(padRight(row.fixture, 44) + padLeft(formatUs(row.medianUs), 12) + padLeft(formatUs(row.p95Us), 11));
|
|
36
|
+
}
|
|
37
|
+
if (outPath) console.log('\nwrote ' + path.relative(rootDir, outPath));
|
|
38
|
+
}
|
|
39
|
+
function finish(packageName, rows) {
|
|
40
|
+
const report = { package: packageName, version: readPackageVersion(), generatedAt: new Date().toISOString(), node: process.version, platform: process.platform + ' ' + process.arch, rounds, rows };
|
|
41
|
+
if (outPath) { fs.mkdirSync(path.dirname(outPath), { recursive: true }); fs.writeFileSync(outPath, JSON.stringify(report, null, 2) + '\n'); }
|
|
42
|
+
printReport(report);
|
|
43
|
+
if (sink === 42) console.log('sink=' + sink);
|
|
44
|
+
}
|
|
45
|
+
function percentile(sorted, fraction) { return sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * fraction) - 1))]; }
|
|
46
|
+
function readPackageVersion() { return JSON.parse(fs.readFileSync(path.join(rootDir, 'package.json'), 'utf8')).version; }
|
|
47
|
+
function parseArgs(argv) { const out = {}; for (let i = 0; i < argv.length; i++) { const arg = argv[i]; if (arg === '--rounds') out.rounds = argv[++i]; else if (arg === '--out') out.out = argv[++i]; else if (arg === '--help' || arg === '-h') { console.log('Usage: npm run bench -- [--rounds 9] [--out benchmarks/results/package-bench.json]'); process.exit(0); } else throw new Error('unknown argument: ' + arg); } return out; }
|
|
48
|
+
function readPositiveInt(value, fallback) { if (value === undefined) return fallback; const number = Number(value); if (!Number.isInteger(number) || number <= 0) throw new Error('expected positive integer, got ' + value); return number; }
|
|
49
|
+
function round(value) { return Math.round(value * 100) / 100; }
|
|
50
|
+
function formatUs(value) { return value >= 1000 ? (value / 1000).toFixed(2) + ' ms' : value.toFixed(2) + ' us'; }
|
|
51
|
+
function padRight(value, width) { return String(value).padEnd(width); }
|
|
52
|
+
function padLeft(value, width) { return String(value).padStart(width); }
|
|
53
|
+
|
|
54
|
+
import { applyPatchImmutable } from '@shapeshift-labs/frontier';
|
|
55
|
+
import { createDiffEngine } from '../dist/index.js';
|
|
56
|
+
|
|
57
|
+
const before = { rows: makeRows(1000), meta: { version: 1 } };
|
|
58
|
+
const after = cloneJson(before);
|
|
59
|
+
after.rows[512] = { ...after.rows[512], score: 9999 };
|
|
60
|
+
after.meta.version = 2;
|
|
61
|
+
const engine = createDiffEngine({ schema: { type: 'array', path: ['rows'], key: 'id', item: { type: 'object', fields: ['id', 'score', 'active', 'label'] } } });
|
|
62
|
+
const patch = engine.diff(before, after);
|
|
63
|
+
const rows = [
|
|
64
|
+
runRow('Engine schema diff, 1k rows', 300, () => { sink += engine.diff(before, after).length; }),
|
|
65
|
+
runRow('Engine apply via core patch', 3000, () => { sink += applyPatchImmutable(before, patch).rows.length; }),
|
|
66
|
+
runRow('Engine equality no-op', 3000, () => { if (engine.equals(before, before)) sink++; }),
|
|
67
|
+
runRow('Engine history encode/decode/apply', 1000, () => { const bytes = engine.encodeHistory([patch]); sink += engine.applyEncodedHistory(before, bytes).rows.length; })
|
|
68
|
+
];
|
|
69
|
+
finish('@shapeshift-labs/frontier-engine', rows);
|
|
70
|
+
function makeRows(count) { const rows = new Array(count); for (let i = 0; i < count; i++) rows[i] = { id: 'row-' + i, score: i, active: (i & 1) === 0, label: 'row ' + i }; return rows; }
|
|
71
|
+
function cloneJson(value) { return JSON.parse(JSON.stringify(value)); }
|
package/dist/engine.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../src/engine.ts"],"names":[],"mappings":"AAgCA,OAAO,KAAK,EACV,UAAU,EAGV,aAAa,EAcd,MAAM,YAAY,CAAC;AA8FpB,wBAAgB,gBAAgB,CAAC,cAAc,CAAC,EAAE,aAAa,GAAG,UAAU,CA0O3E"}
|