@vantage-ui/client 0.0.0-stage → 0.4.4
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/CHANGELOG.md +50 -0
- package/README.md +131 -2
- package/bin/vantage-client.js +5 -0
- package/dist/client-connection.d.ts +41 -0
- package/dist/client-connection.js +260 -0
- package/dist/client-core.d.ts +70 -0
- package/dist/client-core.js +140 -0
- package/dist/client-loading.d.ts +13 -0
- package/dist/client-loading.js +229 -0
- package/dist/client-runs.d.ts +18 -0
- package/dist/client-runs.js +88 -0
- package/dist/client-writes.d.ts +14 -0
- package/dist/client-writes.js +114 -0
- package/dist/client.d.ts +32 -0
- package/dist/client.js +61 -0
- package/dist/codegen/cli.d.ts +8 -0
- package/dist/codegen/cli.js +54 -0
- package/dist/codegen/emit.d.ts +3 -0
- package/dist/codegen/emit.js +190 -0
- package/dist/codegen/fetch.d.ts +4 -0
- package/dist/codegen/fetch.js +41 -0
- package/dist/codegen/meta.d.ts +13 -0
- package/dist/codegen/meta.js +59 -0
- package/dist/codegen/runs.d.ts +20 -0
- package/dist/codegen/runs.js +58 -0
- package/dist/codegen/schema.d.ts +97 -0
- package/dist/codegen/schema.js +23 -0
- package/dist/collection-cell.d.ts +69 -0
- package/dist/collection-cell.js +153 -0
- package/dist/collection.d.ts +32 -0
- package/dist/collection.js +122 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +14 -0
- package/dist/mock-query.d.ts +9 -0
- package/dist/mock-query.js +75 -0
- package/dist/mock-runs.d.ts +61 -0
- package/dist/mock-runs.js +165 -0
- package/dist/mock-store.d.ts +78 -0
- package/dist/mock-store.js +158 -0
- package/dist/mock-transport.d.ts +16 -0
- package/dist/mock-transport.js +38 -0
- package/dist/model.d.ts +5 -0
- package/dist/model.js +169 -0
- package/dist/persist.d.ts +47 -0
- package/dist/persist.js +108 -0
- package/dist/protocol.d.ts +236 -0
- package/dist/protocol.js +5 -0
- package/dist/react.d.ts +59 -0
- package/dist/react.js +169 -0
- package/dist/reserved.d.ts +6 -0
- package/dist/reserved.js +13 -0
- package/dist/run/frames.d.ts +6 -0
- package/dist/run/frames.js +24 -0
- package/dist/run/hooks.d.ts +18 -0
- package/dist/run/hooks.js +87 -0
- package/dist/run/messages.d.ts +20 -0
- package/dist/run/messages.js +42 -0
- package/dist/run/outcome.d.ts +12 -0
- package/dist/run/outcome.js +27 -0
- package/dist/run/run.d.ts +82 -0
- package/dist/run/run.js +246 -0
- package/dist/run/storage.d.ts +3 -0
- package/dist/run/storage.js +20 -0
- package/dist/run/types.d.ts +92 -0
- package/dist/run/types.js +3 -0
- package/dist/run/variables.d.ts +26 -0
- package/dist/run/variables.js +70 -0
- package/dist/schema-meta.d.ts +91 -0
- package/dist/schema-meta.js +3 -0
- package/dist/signal.d.ts +45 -0
- package/dist/signal.js +163 -0
- package/dist/store.d.ts +45 -0
- package/dist/store.js +144 -0
- package/dist/table.d.ts +62 -0
- package/dist/table.js +82 -0
- package/dist/testing.d.ts +61 -0
- package/dist/testing.js +195 -0
- package/dist/transport.d.ts +28 -0
- package/dist/transport.js +32 -0
- package/dist/types.d.ts +159 -0
- package/dist/types.js +2 -0
- package/dist/util.d.ts +18 -0
- package/dist/util.js +46 -0
- package/package.json +56 -4
- package/skills/vantage-client/SKILL.md +390 -0
- package/skills/vantage-client/references/protocol.md +67 -0
- package/skills/vantage-client/references/react.md +242 -0
- package/skills/vantage-client/references/testing.md +173 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { CollectionSpec } from "./protocol.js";
|
|
2
|
+
import { State } from "./signal.js";
|
|
3
|
+
import type { LoadState, Model } from "./types.js";
|
|
4
|
+
import type { Vantage } from "./client.js";
|
|
5
|
+
type AnyModel = Model & Record<string, any>;
|
|
6
|
+
/** A window's cell spec: the collection's, with the window's size as its limit. */
|
|
7
|
+
export type CellSpec = CollectionSpec & {
|
|
8
|
+
$window?: boolean;
|
|
9
|
+
};
|
|
10
|
+
/** One key per description, whatever order its parts were set in. */
|
|
11
|
+
export declare function specKey({ table, where, search, sort, via, limit, $window }: CellSpec): string;
|
|
12
|
+
/** Shared state behind every Collection with the same description. Not exported from the package. */
|
|
13
|
+
export declare class CollectionCell {
|
|
14
|
+
private db;
|
|
15
|
+
readonly spec: CellSpec;
|
|
16
|
+
readonly key: string;
|
|
17
|
+
readonly ids: State<string[]>;
|
|
18
|
+
readonly loadState: State<LoadState>;
|
|
19
|
+
readonly total: State<number | null>;
|
|
20
|
+
readonly error: State<string | null>;
|
|
21
|
+
sub: number | null;
|
|
22
|
+
start: number;
|
|
23
|
+
pinned: boolean;
|
|
24
|
+
/** The server refused to open it; reads don't retry until a resync. */
|
|
25
|
+
refused: boolean;
|
|
26
|
+
touched: number;
|
|
27
|
+
wantedWindow?: {
|
|
28
|
+
start: number;
|
|
29
|
+
count: number;
|
|
30
|
+
};
|
|
31
|
+
private opening?;
|
|
32
|
+
private settled?;
|
|
33
|
+
private firstLoad;
|
|
34
|
+
private modelsCache?;
|
|
35
|
+
private windowWaiters;
|
|
36
|
+
constructor(db: Vantage<any>, spec: CellSpec, key: string);
|
|
37
|
+
/** Something read us: open if closed. After a final refusal of the connection, settle with its message. */
|
|
38
|
+
read(): void;
|
|
39
|
+
/** Open on the server, unless an open is already on its way. The ids shown so far stay until the reply. */
|
|
40
|
+
open(): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* The server dropped every sub (a resync): forget the open in flight, so
|
|
43
|
+
* the next one goes out, and a refusal, so `whenLoaded()` waits for the reopen.
|
|
44
|
+
*/
|
|
45
|
+
reset(): void;
|
|
46
|
+
/** Scrolled away from the start: the first reply's ids aren't the ones on screen. */
|
|
47
|
+
get scrolled(): boolean;
|
|
48
|
+
wanted(): boolean;
|
|
49
|
+
opened(sub: number): void;
|
|
50
|
+
loaded(sub: number, start: number, ids: string[], total: number | null, complete: boolean): void;
|
|
51
|
+
members(start: number, ids: string[], total: number | null, complete: boolean): void;
|
|
52
|
+
failed(message: string): void;
|
|
53
|
+
/** Resolves once rows `start..start+count` have arrived, or the cell failed (refused, or the connection halted). */
|
|
54
|
+
whenWindow(start: number, count: number): Promise<void>;
|
|
55
|
+
/** Whether a `whenLoaded()` is still waiting for its rows. */
|
|
56
|
+
get waitingForWindow(): boolean;
|
|
57
|
+
/**
|
|
58
|
+
* Failed, or the rows from `start` arrived: `count` of them, or fewer when
|
|
59
|
+
* the frame said `complete`. An estimated `total` never ends the window
|
|
60
|
+
* early. Reads state without tracking it.
|
|
61
|
+
*/
|
|
62
|
+
windowSettled(start: number, count: number): boolean;
|
|
63
|
+
/** Resolve every `whenWindow()` whose rows are now here. */
|
|
64
|
+
private wakeWindows;
|
|
65
|
+
drop(id: string): void;
|
|
66
|
+
whenLoaded(): Promise<void>;
|
|
67
|
+
models(): AnyModel[];
|
|
68
|
+
}
|
|
69
|
+
export {};
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// The shared cell behind every Collection with the same description: the
|
|
2
|
+
// server's ordered ids for it, its load state, the sub that keeps it live,
|
|
3
|
+
// and the `window().whenLoaded()` promises waiting on its rows.
|
|
4
|
+
import { State } from "./signal.js";
|
|
5
|
+
/** One key per description, whatever order its parts were set in. */
|
|
6
|
+
export function specKey({ table, where, search, sort, via, limit, $window }) {
|
|
7
|
+
return JSON.stringify({ table, where: where?.length ? where : undefined, search: search || undefined, sort, via, limit, $window });
|
|
8
|
+
}
|
|
9
|
+
/** Shared state behind every Collection with the same description. Not exported from the package. */
|
|
10
|
+
export class CollectionCell {
|
|
11
|
+
db;
|
|
12
|
+
spec;
|
|
13
|
+
key;
|
|
14
|
+
ids;
|
|
15
|
+
loadState = new State("idle");
|
|
16
|
+
total = new State(null);
|
|
17
|
+
error = new State(null);
|
|
18
|
+
sub = null;
|
|
19
|
+
start = 0;
|
|
20
|
+
pinned = false;
|
|
21
|
+
/** The server refused to open it; reads don't retry until a resync. */
|
|
22
|
+
refused = false;
|
|
23
|
+
touched = Date.now();
|
|
24
|
+
wantedWindow;
|
|
25
|
+
opening;
|
|
26
|
+
settled;
|
|
27
|
+
firstLoad;
|
|
28
|
+
modelsCache;
|
|
29
|
+
windowWaiters = [];
|
|
30
|
+
constructor(db, spec, key) {
|
|
31
|
+
this.db = db;
|
|
32
|
+
this.spec = spec;
|
|
33
|
+
this.key = key;
|
|
34
|
+
this.ids = new State([], { observed: () => this.read() });
|
|
35
|
+
this.firstLoad = new Promise((resolve) => (this.settled = resolve));
|
|
36
|
+
}
|
|
37
|
+
/** Something read us: open if closed. After a final refusal of the connection, settle with its message. */
|
|
38
|
+
read() {
|
|
39
|
+
this.touched = Date.now();
|
|
40
|
+
if (this.sub !== null || this.refused)
|
|
41
|
+
return;
|
|
42
|
+
const halted = this.db.haltedWith;
|
|
43
|
+
if (halted !== undefined)
|
|
44
|
+
return this.failed(halted);
|
|
45
|
+
if (this.db.connection.peek().status === "open") {
|
|
46
|
+
if (this.loadState.peek() === "idle")
|
|
47
|
+
this.loadState.set("loading");
|
|
48
|
+
void this.open();
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Open on the server, unless an open is already on its way. The ids shown so far stay until the reply. */
|
|
52
|
+
open() {
|
|
53
|
+
if (!this.opening) {
|
|
54
|
+
const opening = this.db.openCollection(this).finally(() => {
|
|
55
|
+
if (this.opening === opening)
|
|
56
|
+
this.opening = undefined;
|
|
57
|
+
});
|
|
58
|
+
this.opening = opening;
|
|
59
|
+
}
|
|
60
|
+
return this.opening;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The server dropped every sub (a resync): forget the open in flight, so
|
|
64
|
+
* the next one goes out, and a refusal, so `whenLoaded()` waits for the reopen.
|
|
65
|
+
*/
|
|
66
|
+
reset() {
|
|
67
|
+
this.sub = null;
|
|
68
|
+
this.opening = undefined;
|
|
69
|
+
if (!this.refused)
|
|
70
|
+
return;
|
|
71
|
+
this.refused = false;
|
|
72
|
+
this.error.set(null);
|
|
73
|
+
this.loadState.set("loading");
|
|
74
|
+
}
|
|
75
|
+
/** Scrolled away from the start: the first reply's ids aren't the ones on screen. */
|
|
76
|
+
get scrolled() {
|
|
77
|
+
return !!this.wantedWindow && this.wantedWindow.start !== 0;
|
|
78
|
+
}
|
|
79
|
+
wanted() {
|
|
80
|
+
return this.ids.observed || this.loadState.observed || Date.now() - this.touched < this.db.keepAliveMs;
|
|
81
|
+
}
|
|
82
|
+
opened(sub) {
|
|
83
|
+
this.sub = sub;
|
|
84
|
+
this.refused = false;
|
|
85
|
+
this.error.set(null);
|
|
86
|
+
}
|
|
87
|
+
loaded(sub, start, ids, total, complete) {
|
|
88
|
+
this.opened(sub);
|
|
89
|
+
this.members(start, ids, total, complete);
|
|
90
|
+
}
|
|
91
|
+
members(start, ids, total, complete) {
|
|
92
|
+
this.start = start;
|
|
93
|
+
this.ids.set(ids);
|
|
94
|
+
this.total.set(total);
|
|
95
|
+
this.loadState.set(complete ? "complete" : "partial");
|
|
96
|
+
this.settled?.();
|
|
97
|
+
this.wakeWindows();
|
|
98
|
+
}
|
|
99
|
+
failed(message) {
|
|
100
|
+
this.refused = true;
|
|
101
|
+
this.error.set(message);
|
|
102
|
+
this.loadState.set("error");
|
|
103
|
+
this.settled?.();
|
|
104
|
+
this.wakeWindows();
|
|
105
|
+
}
|
|
106
|
+
/** Resolves once rows `start..start+count` have arrived, or the cell failed (refused, or the connection halted). */
|
|
107
|
+
whenWindow(start, count) {
|
|
108
|
+
this.read();
|
|
109
|
+
if (this.windowSettled(start, count))
|
|
110
|
+
return Promise.resolve();
|
|
111
|
+
return new Promise((resolve) => this.windowWaiters.push({ start, count, resolve }));
|
|
112
|
+
}
|
|
113
|
+
/** Whether a `whenLoaded()` is still waiting for its rows. */
|
|
114
|
+
get waitingForWindow() {
|
|
115
|
+
return this.windowWaiters.length > 0;
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* Failed, or the rows from `start` arrived: `count` of them, or fewer when
|
|
119
|
+
* the frame said `complete`. An estimated `total` never ends the window
|
|
120
|
+
* early. Reads state without tracking it.
|
|
121
|
+
*/
|
|
122
|
+
windowSettled(start, count) {
|
|
123
|
+
const state = this.loadState.peek();
|
|
124
|
+
if (state === "error")
|
|
125
|
+
return true;
|
|
126
|
+
if ((state !== "complete" && state !== "partial") || this.start !== start)
|
|
127
|
+
return false;
|
|
128
|
+
return state === "complete" || this.ids.peek().length >= count;
|
|
129
|
+
}
|
|
130
|
+
/** Resolve every `whenWindow()` whose rows are now here. */
|
|
131
|
+
wakeWindows() {
|
|
132
|
+
const waiting = this.windowWaiters;
|
|
133
|
+
this.windowWaiters = waiting.filter((w) => !this.windowSettled(w.start, w.count));
|
|
134
|
+
for (const w of waiting)
|
|
135
|
+
if (this.windowSettled(w.start, w.count))
|
|
136
|
+
w.resolve();
|
|
137
|
+
}
|
|
138
|
+
drop(id) {
|
|
139
|
+
const ids = this.ids.peek();
|
|
140
|
+
if (ids.includes(id))
|
|
141
|
+
this.ids.set(ids.filter((x) => x !== id));
|
|
142
|
+
}
|
|
143
|
+
whenLoaded() {
|
|
144
|
+
this.read();
|
|
145
|
+
return this.firstLoad;
|
|
146
|
+
}
|
|
147
|
+
models() {
|
|
148
|
+
const ids = this.ids.get();
|
|
149
|
+
if (this.modelsCache?.ids !== ids)
|
|
150
|
+
this.modelsCache = { ids, models: ids.map((id) => this.db.model(this.spec.table, id)) };
|
|
151
|
+
return this.modelsCache.models;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { CollectionSpec, Where } from "./protocol.js";
|
|
2
|
+
import type { CollectionWindow, LazyCollection, LoadState, Model } from "./types.js";
|
|
3
|
+
import type { Vantage } from "./client.js";
|
|
4
|
+
type AnyModel = Model & Record<string, any>;
|
|
5
|
+
export declare class Collection implements LazyCollection<AnyModel> {
|
|
6
|
+
private db;
|
|
7
|
+
readonly spec: CollectionSpec;
|
|
8
|
+
/** When it was made; a new one is kept for a while before anything reads it. */
|
|
9
|
+
readonly created: number;
|
|
10
|
+
private cells;
|
|
11
|
+
constructor(db: Vantage<any>, spec: CollectionSpec);
|
|
12
|
+
/** The keys of the cells it has read (its own, and its windows'). */
|
|
13
|
+
cellKeys(): string[];
|
|
14
|
+
private cellFor;
|
|
15
|
+
private get cell();
|
|
16
|
+
get elements(): readonly AnyModel[];
|
|
17
|
+
get length(): number;
|
|
18
|
+
get loadState(): LoadState;
|
|
19
|
+
get total(): number | null;
|
|
20
|
+
get totalExact(): boolean;
|
|
21
|
+
get error(): string | null;
|
|
22
|
+
map<U>(fn: (item: AnyModel, index: number) => U): U[];
|
|
23
|
+
forEach(fn: (item: AnyModel, index: number) => void): void;
|
|
24
|
+
[Symbol.iterator](): ArrayIterator<AnyModel>;
|
|
25
|
+
where(conditions: Partial<Record<string, unknown>> | Where[]): LazyCollection<AnyModel>;
|
|
26
|
+
sort(column: string, direction?: "asc" | "desc"): LazyCollection<AnyModel>;
|
|
27
|
+
search(text: string): LazyCollection<AnyModel>;
|
|
28
|
+
limit(rows: number): LazyCollection<AnyModel>;
|
|
29
|
+
window(start: number, count: number): CollectionWindow<AnyModel>;
|
|
30
|
+
load(): Promise<LazyCollection<AnyModel>>;
|
|
31
|
+
}
|
|
32
|
+
export {};
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// Collections: a description (table, where, sort, search, via, window) read
|
|
2
|
+
// through the shared CollectionCell that holds the server's ids for it.
|
|
3
|
+
// `db.collection(spec)` keeps one Collection per description.
|
|
4
|
+
export class Collection {
|
|
5
|
+
db;
|
|
6
|
+
spec;
|
|
7
|
+
/** When it was made; a new one is kept for a while before anything reads it. */
|
|
8
|
+
created = Date.now();
|
|
9
|
+
cells = new Set();
|
|
10
|
+
constructor(db, spec) {
|
|
11
|
+
this.db = db;
|
|
12
|
+
this.spec = spec;
|
|
13
|
+
}
|
|
14
|
+
/** The keys of the cells it has read (its own, and its windows'). */
|
|
15
|
+
cellKeys() {
|
|
16
|
+
return [...this.cells];
|
|
17
|
+
}
|
|
18
|
+
cellFor(spec) {
|
|
19
|
+
const cell = this.db.collectionCell(spec);
|
|
20
|
+
this.cells.add(cell.key);
|
|
21
|
+
return cell;
|
|
22
|
+
}
|
|
23
|
+
get cell() {
|
|
24
|
+
const cell = this.cellFor(this.spec);
|
|
25
|
+
cell.read();
|
|
26
|
+
return cell;
|
|
27
|
+
}
|
|
28
|
+
get elements() {
|
|
29
|
+
return this.cell.models();
|
|
30
|
+
}
|
|
31
|
+
get length() {
|
|
32
|
+
const cell = this.cell;
|
|
33
|
+
const ids = cell.ids.get();
|
|
34
|
+
return cell.loadState.get() === "complete" ? ids.length : (cell.total.get() ?? ids.length);
|
|
35
|
+
}
|
|
36
|
+
get loadState() {
|
|
37
|
+
return this.cell.loadState.get();
|
|
38
|
+
}
|
|
39
|
+
get total() {
|
|
40
|
+
return this.cell.total.get();
|
|
41
|
+
}
|
|
42
|
+
get totalExact() {
|
|
43
|
+
const cell = this.cell;
|
|
44
|
+
return cell.loadState.get() === "complete" && cell.total.get() !== null;
|
|
45
|
+
}
|
|
46
|
+
get error() {
|
|
47
|
+
return this.cell.error.get();
|
|
48
|
+
}
|
|
49
|
+
map(fn) {
|
|
50
|
+
return this.elements.map(fn);
|
|
51
|
+
}
|
|
52
|
+
forEach(fn) {
|
|
53
|
+
this.elements.forEach(fn);
|
|
54
|
+
}
|
|
55
|
+
[Symbol.iterator]() {
|
|
56
|
+
return this.elements[Symbol.iterator]();
|
|
57
|
+
}
|
|
58
|
+
where(conditions) {
|
|
59
|
+
const meta = this.db.meta(this.spec.table);
|
|
60
|
+
const added = Array.isArray(conditions)
|
|
61
|
+
? conditions
|
|
62
|
+
: Object.entries(conditions).map(([k, v]) => {
|
|
63
|
+
const column = meta.references[k]?.column ?? k;
|
|
64
|
+
if (Array.isArray(v))
|
|
65
|
+
return [column, "in", v];
|
|
66
|
+
if (v && typeof v === "object" && !Array.isArray(v)) {
|
|
67
|
+
const [op, operand] = Object.entries(v)[0];
|
|
68
|
+
return [column, op, operand];
|
|
69
|
+
}
|
|
70
|
+
return [column, "eq", v];
|
|
71
|
+
});
|
|
72
|
+
return this.db.collection({ ...this.spec, where: [...(this.spec.where ?? []), ...added] });
|
|
73
|
+
}
|
|
74
|
+
sort(column, direction = "asc") {
|
|
75
|
+
return this.db.collection({ ...this.spec, sort: [column, direction] });
|
|
76
|
+
}
|
|
77
|
+
search(text) {
|
|
78
|
+
return this.db.collection({ ...this.spec, search: text || undefined });
|
|
79
|
+
}
|
|
80
|
+
limit(rows) {
|
|
81
|
+
return this.db.collection({ ...this.spec, limit: rows });
|
|
82
|
+
}
|
|
83
|
+
window(start, count) {
|
|
84
|
+
const cell = this.cellFor({ ...this.spec, limit: count, $window: true });
|
|
85
|
+
const moved = !cell.wantedWindow || cell.wantedWindow.start !== start || cell.wantedWindow.count !== count;
|
|
86
|
+
cell.wantedWindow = { start, count };
|
|
87
|
+
cell.read();
|
|
88
|
+
if (moved && cell.sub !== null)
|
|
89
|
+
this.db.moveWindow(cell);
|
|
90
|
+
const ids = cell.ids.get();
|
|
91
|
+
const loadState = cell.loadState.get();
|
|
92
|
+
const total = cell.total.get();
|
|
93
|
+
const error = cell.error.get();
|
|
94
|
+
const items = Array.from({ length: count }, (_, i) => {
|
|
95
|
+
const at = start + i - cell.start;
|
|
96
|
+
const id = at >= 0 && at < ids.length ? ids[at] : undefined;
|
|
97
|
+
return id ? this.db.model(this.spec.table, id) : undefined;
|
|
98
|
+
});
|
|
99
|
+
const gap = items.indexOf(undefined);
|
|
100
|
+
const end = start + (gap === -1 ? count : gap); // rows arrived from `start` without a gap
|
|
101
|
+
const settled = cell.windowSettled(start, count);
|
|
102
|
+
// The last `rows`/`members` frame said `complete`: the server sent every row.
|
|
103
|
+
const totalExact = loadState === "complete" && total !== null;
|
|
104
|
+
return {
|
|
105
|
+
start,
|
|
106
|
+
count,
|
|
107
|
+
items,
|
|
108
|
+
at: (index) => items[index - start],
|
|
109
|
+
error,
|
|
110
|
+
total,
|
|
111
|
+
totalExact,
|
|
112
|
+
settled,
|
|
113
|
+
length: total ?? end,
|
|
114
|
+
lengthExact: total !== null ? totalExact : settled && error === null && (loadState === "complete" || end < start + count),
|
|
115
|
+
whenLoaded: () => cell.whenWindow(start, count).then(() => this.window(start, count)),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
async load() {
|
|
119
|
+
await this.cellFor(this.spec).whenLoaded();
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export { createVantage } from "./client.js";
|
|
2
|
+
export type { Vantage, VantageDb, VantageOptions, TableHandle, Tables, ProjectActions } from "./client.js";
|
|
3
|
+
export { State, batch, autorun } from "./signal.js";
|
|
4
|
+
export type { Transport } from "./transport.js";
|
|
5
|
+
export { MemoryStorage, IndexedDBStorage, type Storage } from "./persist.js";
|
|
6
|
+
export type { ActionError, ActionResult, CollectionWindow, Connection, ConnectionStatus, FieldMeta, Hydrated, LazyCollection, LazyReference, LoadState, Model, Presence, RelationKeys, RowStatus, SchemaMeta, SchemaTypes, Servo, TableMeta, WritableFields, } from "./types.js";
|
|
7
|
+
export type { CollectionSpec, Where, Sort, WireRecord, Json, RunCode } from "./protocol.js";
|
|
8
|
+
export type { Run, RunError, RunFactory, RunMessageOf, RunStatus, RunTypes, StartOptions } from "./run/types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// @vantage-ui/client — a vantage-ui project as a typed, live object graph.
|
|
2
|
+
//
|
|
3
|
+
// import { createVantage } from "@vantage-ui/client";
|
|
4
|
+
// import { schema, type Schema } from "./vantage.gen";
|
|
5
|
+
//
|
|
6
|
+
// const db = createVantage<Schema>({ schema, token }); // ws(s)://<page host>/v1/ws
|
|
7
|
+
// db.order.sort("created_at", "desc").map((o) => o.client.value?.name);
|
|
8
|
+
// await order.cancel_order({ reason: "out_of_stock" });
|
|
9
|
+
// const run = db.run.restock.start({ bakery }); // run.state, run.on(…), await run.done
|
|
10
|
+
//
|
|
11
|
+
// React bindings: "@vantage-ui/client/react". Tests: "@vantage-ui/client/testing".
|
|
12
|
+
export { createVantage } from "./client.js";
|
|
13
|
+
export { State, batch, autorun } from "./signal.js";
|
|
14
|
+
export { MemoryStorage, IndexedDBStorage } from "./persist.js";
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { CollectionSpec } from "./protocol.js";
|
|
2
|
+
import type { SchemaMeta } from "./types.js";
|
|
3
|
+
export type Row = Record<string, unknown> & {
|
|
4
|
+
id: string;
|
|
5
|
+
};
|
|
6
|
+
/** The ids of `rows` that `spec` selects, in its order. */
|
|
7
|
+
export declare function query(schema: SchemaMeta, rows: Row[], spec: CollectionSpec): string[];
|
|
8
|
+
/** A minimal schema for one table whose columns are the keys of `rows`. */
|
|
9
|
+
export declare function tableSchema(table: string, rows: Row[]): SchemaMeta;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
// How the MockServer answers a collection: filter, traverse, search and
|
|
2
|
+
// sort like vantage-server does.
|
|
3
|
+
/** The ids of `rows` that `spec` selects, in its order. */
|
|
4
|
+
export function query(schema, rows, spec) {
|
|
5
|
+
for (const w of spec.where ?? [])
|
|
6
|
+
rows = rows.filter((r) => matches(r[w[0]], w));
|
|
7
|
+
if (spec.via) {
|
|
8
|
+
const fk = schema.tables[spec.via.table]?.hasMany[spec.via.ref]?.foreignKey;
|
|
9
|
+
rows = rows.filter((r) => fk && r[fk] === spec.via.id);
|
|
10
|
+
}
|
|
11
|
+
if (spec.search) {
|
|
12
|
+
const q = spec.search.toLowerCase();
|
|
13
|
+
rows = rows.filter((r) => Object.values(r).some((v) => String(v ?? "").toLowerCase().includes(q)));
|
|
14
|
+
}
|
|
15
|
+
if (spec.sort) {
|
|
16
|
+
const [col, dir] = spec.sort;
|
|
17
|
+
rows = [...rows].sort((a, b) => (dir === "desc" ? -1 : 1) * compare(a[col], b[col]));
|
|
18
|
+
}
|
|
19
|
+
return rows.map((r) => r.id);
|
|
20
|
+
}
|
|
21
|
+
function matches(value, [, op, operand]) {
|
|
22
|
+
switch (op) {
|
|
23
|
+
case "eq":
|
|
24
|
+
return value === operand;
|
|
25
|
+
case "ne":
|
|
26
|
+
return value !== operand;
|
|
27
|
+
case "gt":
|
|
28
|
+
return compare(value, operand) > 0;
|
|
29
|
+
case "gte":
|
|
30
|
+
return compare(value, operand) >= 0;
|
|
31
|
+
case "lt":
|
|
32
|
+
return compare(value, operand) < 0;
|
|
33
|
+
case "lte":
|
|
34
|
+
return compare(value, operand) <= 0;
|
|
35
|
+
case "in":
|
|
36
|
+
return Array.isArray(operand) && operand.includes(value);
|
|
37
|
+
case "not_in":
|
|
38
|
+
return Array.isArray(operand) && !operand.includes(value);
|
|
39
|
+
case "like":
|
|
40
|
+
return String(value ?? "").toLowerCase().includes(String(operand).toLowerCase());
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function compare(a, b) {
|
|
44
|
+
if (a == null)
|
|
45
|
+
return b == null ? 0 : 1;
|
|
46
|
+
if (b == null)
|
|
47
|
+
return -1;
|
|
48
|
+
if (typeof a === "number" && typeof b === "number")
|
|
49
|
+
return a - b;
|
|
50
|
+
return String(a).localeCompare(String(b));
|
|
51
|
+
}
|
|
52
|
+
/** A minimal schema for one table whose columns are the keys of `rows`. */
|
|
53
|
+
export function tableSchema(table, rows) {
|
|
54
|
+
const columns = Object.fromEntries([...new Set(["id", ...rows.flatMap((r) => Object.keys(r))])].map((c) => [c, { type: "string" }]));
|
|
55
|
+
return {
|
|
56
|
+
hash: "mock",
|
|
57
|
+
tables: {
|
|
58
|
+
[table]: {
|
|
59
|
+
key: table,
|
|
60
|
+
load: "lazy",
|
|
61
|
+
persist: false,
|
|
62
|
+
live: "push",
|
|
63
|
+
readOnly: false,
|
|
64
|
+
idColumn: "id",
|
|
65
|
+
title: null,
|
|
66
|
+
collectionLimit: 500,
|
|
67
|
+
columns,
|
|
68
|
+
references: {},
|
|
69
|
+
hasMany: {},
|
|
70
|
+
actions: {},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
actions: {},
|
|
74
|
+
};
|
|
75
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import type { MockTransport } from "./mock-transport.js";
|
|
2
|
+
import type { ClientFrame, Json, ServerFrame } from "./protocol.js";
|
|
3
|
+
import type { RunMeta } from "./schema-meta.js";
|
|
4
|
+
/** What a handler's run can do, like a body's run vocabulary. */
|
|
5
|
+
export interface MockRunControl {
|
|
6
|
+
readonly id: string;
|
|
7
|
+
get(name: string): unknown;
|
|
8
|
+
/** `state.<name> = value`: a patch to every follower. */
|
|
9
|
+
set(name: string, value: unknown): void;
|
|
10
|
+
/** Several variables in one patch, as the server's throttle coalesces them. */
|
|
11
|
+
update(values: Record<string, unknown>): void;
|
|
12
|
+
emit(name: string, payload?: unknown): void;
|
|
13
|
+
/** `notify(text)`: the `vantage.notify` message. */
|
|
14
|
+
notify(text: string): void;
|
|
15
|
+
/** Resolves once `state.<name>` is truthy; rejects when the run is cancelled. */
|
|
16
|
+
wait(name: string): Promise<unknown>;
|
|
17
|
+
readonly cancelled: boolean;
|
|
18
|
+
}
|
|
19
|
+
type Status = "running" | "done" | "failed" | "cancelled";
|
|
20
|
+
type Invoke = Extract<ClientFrame, {
|
|
21
|
+
t: "invoke";
|
|
22
|
+
}>;
|
|
23
|
+
export declare class MockRun implements MockRunControl {
|
|
24
|
+
readonly id: string;
|
|
25
|
+
readonly action: string;
|
|
26
|
+
readonly rid: string | undefined;
|
|
27
|
+
readonly args: Record<string, unknown>;
|
|
28
|
+
readonly meta: RunMeta;
|
|
29
|
+
status: Status;
|
|
30
|
+
result: unknown;
|
|
31
|
+
error: string | undefined;
|
|
32
|
+
readonly state: Record<string, unknown>;
|
|
33
|
+
readonly messages: {
|
|
34
|
+
name: string;
|
|
35
|
+
payload: Json;
|
|
36
|
+
seq: number;
|
|
37
|
+
}[];
|
|
38
|
+
private seq;
|
|
39
|
+
private followers;
|
|
40
|
+
private waiters;
|
|
41
|
+
constructor(id: string, action: string, rid: string | undefined, args: Record<string, unknown>, meta: RunMeta);
|
|
42
|
+
get cancelled(): boolean;
|
|
43
|
+
get finished(): boolean;
|
|
44
|
+
get(name: string): unknown;
|
|
45
|
+
set(name: string, value: unknown): void;
|
|
46
|
+
update(values: Record<string, unknown>): void;
|
|
47
|
+
emit(name: string, payload?: unknown): void;
|
|
48
|
+
notify(text: string): void;
|
|
49
|
+
wait(name: string): Promise<unknown>;
|
|
50
|
+
private outcome;
|
|
51
|
+
private send;
|
|
52
|
+
}
|
|
53
|
+
/** The answer to `invoke`, as vantage-server gives it. `start` runs a new run's handler. */
|
|
54
|
+
export declare function invokeRun(runs: Map<string, MockRun>, client: MockTransport, id: number, frame: Invoke, start: (frame: Invoke & {
|
|
55
|
+
action: string;
|
|
56
|
+
}) => MockRun | ServerFrame): void;
|
|
57
|
+
/** `run_set`: inputs only, of their type. */
|
|
58
|
+
export declare function setInput(run: MockRun | undefined, id: number, frame: Extract<ClientFrame, {
|
|
59
|
+
t: "run_set";
|
|
60
|
+
}>): ServerFrame;
|
|
61
|
+
export {};
|