@vantage-ui/client 0.0.0-stage → 0.4.3
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 +46 -0
- 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 +54 -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
- package/README.md +0 -3
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/** The shape `vantage-client generate` emits as `Schema`. */
|
|
2
|
+
export interface SchemaTypes {
|
|
3
|
+
models: object;
|
|
4
|
+
fields: object;
|
|
5
|
+
inputs: object;
|
|
6
|
+
tableActions: object;
|
|
7
|
+
projectActions: object;
|
|
8
|
+
}
|
|
9
|
+
/** Runtime metadata (`schema` const in vantage.gen.ts). */
|
|
10
|
+
export interface SchemaMeta {
|
|
11
|
+
readonly hash: string;
|
|
12
|
+
readonly tables: {
|
|
13
|
+
readonly [table: string]: TableMeta;
|
|
14
|
+
};
|
|
15
|
+
readonly actions: {
|
|
16
|
+
readonly [action: string]: ActionMeta;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
/** A project action a client can run. */
|
|
20
|
+
export interface ActionMeta extends RunMeta {
|
|
21
|
+
readonly key: string;
|
|
22
|
+
readonly kind?: string;
|
|
23
|
+
readonly fields: readonly FieldMeta[];
|
|
24
|
+
}
|
|
25
|
+
/** What a run of an action shares: its variables, and the messages it may send. Absent in older generated files. */
|
|
26
|
+
export interface RunMeta {
|
|
27
|
+
readonly state?: {
|
|
28
|
+
readonly [name: string]: {
|
|
29
|
+
readonly type: string;
|
|
30
|
+
readonly default: unknown;
|
|
31
|
+
readonly input: boolean;
|
|
32
|
+
};
|
|
33
|
+
};
|
|
34
|
+
readonly messages?: readonly string[];
|
|
35
|
+
}
|
|
36
|
+
export interface TableMeta {
|
|
37
|
+
readonly key: string;
|
|
38
|
+
readonly load: "instant" | "lazy" | "manual";
|
|
39
|
+
readonly persist: boolean;
|
|
40
|
+
readonly live: unknown;
|
|
41
|
+
readonly readOnly: boolean;
|
|
42
|
+
readonly idColumn: string;
|
|
43
|
+
readonly title: string | null;
|
|
44
|
+
readonly collectionLimit: number;
|
|
45
|
+
/** Columns a collection may sort by; null when any column may be. */
|
|
46
|
+
readonly sortable?: readonly string[] | null;
|
|
47
|
+
/** Columns `where` may name. */
|
|
48
|
+
readonly filter?: readonly string[] | null;
|
|
49
|
+
/** The sort of a collection opened without one. */
|
|
50
|
+
readonly defaultSort?: {
|
|
51
|
+
readonly column: string;
|
|
52
|
+
readonly direction: "asc" | "desc";
|
|
53
|
+
} | null;
|
|
54
|
+
readonly columns: {
|
|
55
|
+
readonly [column: string]: {
|
|
56
|
+
readonly type: string;
|
|
57
|
+
readonly readonly?: boolean;
|
|
58
|
+
readonly hidden?: boolean;
|
|
59
|
+
readonly enum?: readonly unknown[];
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
readonly references: {
|
|
63
|
+
readonly [name: string]: {
|
|
64
|
+
readonly column: string;
|
|
65
|
+
readonly table: string;
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
readonly hasMany: {
|
|
69
|
+
readonly [name: string]: {
|
|
70
|
+
readonly table: string;
|
|
71
|
+
readonly foreignKey: string;
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
readonly actions: {
|
|
75
|
+
readonly [name: string]: RunMeta & {
|
|
76
|
+
readonly perRow: boolean;
|
|
77
|
+
readonly label: string;
|
|
78
|
+
readonly fields: readonly FieldMeta[];
|
|
79
|
+
};
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
export interface FieldMeta {
|
|
83
|
+
readonly name: string;
|
|
84
|
+
readonly type: string;
|
|
85
|
+
readonly label: string | null;
|
|
86
|
+
readonly required: boolean;
|
|
87
|
+
readonly choices: readonly string[] | null;
|
|
88
|
+
readonly default: unknown;
|
|
89
|
+
readonly widget: string | null;
|
|
90
|
+
readonly options: boolean;
|
|
91
|
+
}
|
package/dist/signal.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
type Subscriber = () => void;
|
|
2
|
+
export interface StateHooks {
|
|
3
|
+
/** First subscriber attached. */
|
|
4
|
+
observed?(): void;
|
|
5
|
+
/** Last subscriber left. */
|
|
6
|
+
unobserved?(): void;
|
|
7
|
+
}
|
|
8
|
+
export declare class State<T> {
|
|
9
|
+
private value;
|
|
10
|
+
private readonly hooks?;
|
|
11
|
+
private subscribers;
|
|
12
|
+
/** Bumps on every change; lets a late subscriber detect a missed change. */
|
|
13
|
+
version: number;
|
|
14
|
+
constructor(value: T, hooks?: StateHooks | undefined);
|
|
15
|
+
get(): T;
|
|
16
|
+
peek(): T;
|
|
17
|
+
set(value: T): void;
|
|
18
|
+
/** Mark changed without replacing the value (mutated in place). */
|
|
19
|
+
touch(): void;
|
|
20
|
+
get observed(): boolean;
|
|
21
|
+
subscribe(fn: Subscriber): () => void;
|
|
22
|
+
private notify;
|
|
23
|
+
}
|
|
24
|
+
export declare function batch<T>(fn: () => T): T;
|
|
25
|
+
/** Run `fn`, collecting the states it reads (with their versions). */
|
|
26
|
+
export declare function track<T>(fn: () => T): [T, Map<State<unknown>, number>];
|
|
27
|
+
/** Read without recording dependencies. */
|
|
28
|
+
export declare function untracked<T>(fn: () => T): T;
|
|
29
|
+
/** A tracked computation with listeners: the engine behind `observer()`. */
|
|
30
|
+
export declare class Reaction {
|
|
31
|
+
private deps;
|
|
32
|
+
private unsubscribers;
|
|
33
|
+
private listeners;
|
|
34
|
+
version: number;
|
|
35
|
+
/** Run `fn` and make its reads this reaction's dependencies. */
|
|
36
|
+
track<T>(fn: () => T): T;
|
|
37
|
+
subscribe: (listener: Subscriber) => (() => void);
|
|
38
|
+
getVersion: () => number;
|
|
39
|
+
dispose(): void;
|
|
40
|
+
private sync;
|
|
41
|
+
private changed;
|
|
42
|
+
}
|
|
43
|
+
/** Re-run `fn` whenever what it read changes. Returns a stop function. */
|
|
44
|
+
export declare function autorun(fn: () => void): () => void;
|
|
45
|
+
export {};
|
package/dist/signal.js
ADDED
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
// Fine-grained reactivity, about 150 lines.
|
|
2
|
+
//
|
|
3
|
+
// - `State` holds a value. Reading it inside `track()` records a dependency.
|
|
4
|
+
// - `batch()` delays notifications until the outermost batch ends, so one
|
|
5
|
+
// server frame touching forty fields notifies each subscriber once.
|
|
6
|
+
// - `Reaction` is what a component render (or `useLive`) tracks with. It
|
|
7
|
+
// subscribes to its dependencies only while something listens to it, so a
|
|
8
|
+
// render React throws away never leaks subscriptions.
|
|
9
|
+
let tracking = null;
|
|
10
|
+
let batchDepth = 0;
|
|
11
|
+
const queued = new Set();
|
|
12
|
+
export class State {
|
|
13
|
+
value;
|
|
14
|
+
hooks;
|
|
15
|
+
subscribers = new Set();
|
|
16
|
+
/** Bumps on every change; lets a late subscriber detect a missed change. */
|
|
17
|
+
version = 0;
|
|
18
|
+
constructor(value, hooks) {
|
|
19
|
+
this.value = value;
|
|
20
|
+
this.hooks = hooks;
|
|
21
|
+
}
|
|
22
|
+
get() {
|
|
23
|
+
if (tracking && !tracking.has(this))
|
|
24
|
+
tracking.set(this, this.version);
|
|
25
|
+
return this.value;
|
|
26
|
+
}
|
|
27
|
+
peek() {
|
|
28
|
+
return this.value;
|
|
29
|
+
}
|
|
30
|
+
set(value) {
|
|
31
|
+
if (Object.is(value, this.value))
|
|
32
|
+
return;
|
|
33
|
+
this.value = value;
|
|
34
|
+
this.version++;
|
|
35
|
+
this.notify();
|
|
36
|
+
}
|
|
37
|
+
/** Mark changed without replacing the value (mutated in place). */
|
|
38
|
+
touch() {
|
|
39
|
+
this.version++;
|
|
40
|
+
this.notify();
|
|
41
|
+
}
|
|
42
|
+
get observed() {
|
|
43
|
+
return this.subscribers.size > 0;
|
|
44
|
+
}
|
|
45
|
+
subscribe(fn) {
|
|
46
|
+
if (this.subscribers.size === 0)
|
|
47
|
+
this.hooks?.observed?.();
|
|
48
|
+
this.subscribers.add(fn);
|
|
49
|
+
return () => {
|
|
50
|
+
if (this.subscribers.delete(fn) && this.subscribers.size === 0)
|
|
51
|
+
this.hooks?.unobserved?.();
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
notify() {
|
|
55
|
+
for (const fn of this.subscribers) {
|
|
56
|
+
if (batchDepth > 0)
|
|
57
|
+
queued.add(fn);
|
|
58
|
+
else
|
|
59
|
+
fn();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export function batch(fn) {
|
|
64
|
+
batchDepth++;
|
|
65
|
+
try {
|
|
66
|
+
return fn();
|
|
67
|
+
}
|
|
68
|
+
finally {
|
|
69
|
+
if (--batchDepth === 0) {
|
|
70
|
+
const run = [...queued];
|
|
71
|
+
queued.clear();
|
|
72
|
+
for (const fn of run)
|
|
73
|
+
fn();
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
/** Run `fn`, collecting the states it reads (with their versions). */
|
|
78
|
+
export function track(fn) {
|
|
79
|
+
const previous = tracking;
|
|
80
|
+
const deps = new Map();
|
|
81
|
+
tracking = deps;
|
|
82
|
+
try {
|
|
83
|
+
return [fn(), deps];
|
|
84
|
+
}
|
|
85
|
+
finally {
|
|
86
|
+
tracking = previous;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
/** Read without recording dependencies. */
|
|
90
|
+
export function untracked(fn) {
|
|
91
|
+
const previous = tracking;
|
|
92
|
+
tracking = null;
|
|
93
|
+
try {
|
|
94
|
+
return fn();
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
tracking = previous;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/** A tracked computation with listeners: the engine behind `observer()`. */
|
|
101
|
+
export class Reaction {
|
|
102
|
+
deps = new Map();
|
|
103
|
+
unsubscribers = new Map();
|
|
104
|
+
listeners = new Set();
|
|
105
|
+
version = 0;
|
|
106
|
+
/** Run `fn` and make its reads this reaction's dependencies. */
|
|
107
|
+
track(fn) {
|
|
108
|
+
const [result, deps] = track(fn);
|
|
109
|
+
this.deps = deps;
|
|
110
|
+
if (this.listeners.size > 0)
|
|
111
|
+
this.sync();
|
|
112
|
+
return result;
|
|
113
|
+
}
|
|
114
|
+
subscribe = (listener) => {
|
|
115
|
+
this.listeners.add(listener);
|
|
116
|
+
if (this.listeners.size === 1) {
|
|
117
|
+
this.sync();
|
|
118
|
+
// A dependency changed between tracking and subscribing.
|
|
119
|
+
for (const [state, seen] of this.deps) {
|
|
120
|
+
if (state.version !== seen) {
|
|
121
|
+
this.changed();
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return () => {
|
|
127
|
+
this.listeners.delete(listener);
|
|
128
|
+
if (this.listeners.size === 0)
|
|
129
|
+
this.dispose();
|
|
130
|
+
};
|
|
131
|
+
};
|
|
132
|
+
getVersion = () => this.version;
|
|
133
|
+
dispose() {
|
|
134
|
+
for (const unsubscribe of this.unsubscribers.values())
|
|
135
|
+
unsubscribe();
|
|
136
|
+
this.unsubscribers.clear();
|
|
137
|
+
}
|
|
138
|
+
sync() {
|
|
139
|
+
for (const [state, unsubscribe] of this.unsubscribers) {
|
|
140
|
+
if (!this.deps.has(state)) {
|
|
141
|
+
unsubscribe();
|
|
142
|
+
this.unsubscribers.delete(state);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
for (const state of this.deps.keys()) {
|
|
146
|
+
if (!this.unsubscribers.has(state))
|
|
147
|
+
this.unsubscribers.set(state, state.subscribe(this.changed));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
changed = () => {
|
|
151
|
+
this.version++;
|
|
152
|
+
for (const listener of [...this.listeners])
|
|
153
|
+
listener();
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
/** Re-run `fn` whenever what it read changes. Returns a stop function. */
|
|
157
|
+
export function autorun(fn) {
|
|
158
|
+
const reaction = new Reaction();
|
|
159
|
+
const run = () => reaction.track(fn);
|
|
160
|
+
const stop = reaction.subscribe(run);
|
|
161
|
+
run();
|
|
162
|
+
return stop;
|
|
163
|
+
}
|
package/dist/store.d.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { State } from "./signal.js";
|
|
2
|
+
import type { WireRecord } from "./protocol.js";
|
|
3
|
+
export type RowStatus = "fresh" | "stale" | "pending" | "failed";
|
|
4
|
+
export type Presence = "none" | "loading" | "loaded" | "missing" | "error";
|
|
5
|
+
export declare class RecordCell {
|
|
6
|
+
readonly table: string;
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly fields: Map<string, State<unknown>>;
|
|
9
|
+
readonly status: State<RowStatus>;
|
|
10
|
+
readonly presence: State<Presence>;
|
|
11
|
+
readonly errors: State<Record<string, string>>;
|
|
12
|
+
/** The server's message when it refused to load this record (`presence` is `error`). */
|
|
13
|
+
readonly loadError: State<string | null>;
|
|
14
|
+
/** A reload failed: the next read fetches it again. */
|
|
15
|
+
reload: boolean;
|
|
16
|
+
/** Bumps whenever any field changes (for `useRecord`, persistence). */
|
|
17
|
+
readonly version: State<number>;
|
|
18
|
+
baseline: Record<string, unknown>;
|
|
19
|
+
readonly pending: Map<string, unknown>;
|
|
20
|
+
/** Last time something read this cell; eviction keeps recent ones. */
|
|
21
|
+
touched: number;
|
|
22
|
+
constructor(table: string, id: string);
|
|
23
|
+
field(name: string): State<unknown>;
|
|
24
|
+
get(name: string): unknown;
|
|
25
|
+
/** The record as the UI sees it now (pending over baseline). */
|
|
26
|
+
snapshot(): Record<string, unknown>;
|
|
27
|
+
/** The server's version arrived. */
|
|
28
|
+
applyServer(record: WireRecord, status?: RowStatus): void;
|
|
29
|
+
setPending(patch: Record<string, unknown>): void;
|
|
30
|
+
/** Drop pending edits for `keys` (confirmed or rejected). */
|
|
31
|
+
settle(keys: string[], status: RowStatus, errors?: Record<string, string>): void;
|
|
32
|
+
/** The server refused to load it. */
|
|
33
|
+
refused(message: string): void;
|
|
34
|
+
markRemoved(): void;
|
|
35
|
+
private effective;
|
|
36
|
+
private refresh;
|
|
37
|
+
}
|
|
38
|
+
export declare class Store {
|
|
39
|
+
private tables;
|
|
40
|
+
cell(table: string, id: string): RecordCell;
|
|
41
|
+
peek(table: string, id: string): RecordCell | undefined;
|
|
42
|
+
cells(table: string): IterableIterator<RecordCell>;
|
|
43
|
+
allCells(): RecordCell[];
|
|
44
|
+
evict(table: string, id: string): void;
|
|
45
|
+
}
|
package/dist/store.js
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// The record store: one cell per (table, id).
|
|
2
|
+
//
|
|
3
|
+
// A cell keeps the server's last confirmed record (the baseline) and the
|
|
4
|
+
// local edits in flight (pending). Readers see pending laid over baseline,
|
|
5
|
+
// per field, through one `State` per field — so a component that shows
|
|
6
|
+
// `order.status` re-renders when the status changes and not when the note
|
|
7
|
+
// does. A remote change updates the baseline and the pending edits stay on
|
|
8
|
+
// top (Linear's rebase; the browser twin of Diorama's PendingFlashes).
|
|
9
|
+
import { State, batch } from "./signal.js";
|
|
10
|
+
export class RecordCell {
|
|
11
|
+
table;
|
|
12
|
+
id;
|
|
13
|
+
fields = new Map();
|
|
14
|
+
status = new State("fresh");
|
|
15
|
+
presence = new State("none");
|
|
16
|
+
errors = new State({});
|
|
17
|
+
/** The server's message when it refused to load this record (`presence` is `error`). */
|
|
18
|
+
loadError = new State(null);
|
|
19
|
+
/** A reload failed: the next read fetches it again. */
|
|
20
|
+
reload = false;
|
|
21
|
+
/** Bumps whenever any field changes (for `useRecord`, persistence). */
|
|
22
|
+
version = new State(0);
|
|
23
|
+
baseline = {};
|
|
24
|
+
pending = new Map();
|
|
25
|
+
/** Last time something read this cell; eviction keeps recent ones. */
|
|
26
|
+
touched = Date.now();
|
|
27
|
+
constructor(table, id) {
|
|
28
|
+
this.table = table;
|
|
29
|
+
this.id = id;
|
|
30
|
+
}
|
|
31
|
+
field(name) {
|
|
32
|
+
let state = this.fields.get(name);
|
|
33
|
+
if (!state) {
|
|
34
|
+
state = new State(this.effective(name));
|
|
35
|
+
this.fields.set(name, state);
|
|
36
|
+
}
|
|
37
|
+
return state;
|
|
38
|
+
}
|
|
39
|
+
get(name) {
|
|
40
|
+
this.touched = Date.now();
|
|
41
|
+
return this.field(name).get();
|
|
42
|
+
}
|
|
43
|
+
/** The record as the UI sees it now (pending over baseline). */
|
|
44
|
+
snapshot() {
|
|
45
|
+
const out = { ...this.baseline };
|
|
46
|
+
for (const [k, v] of this.pending)
|
|
47
|
+
out[k] = v;
|
|
48
|
+
out.id = this.id;
|
|
49
|
+
return out;
|
|
50
|
+
}
|
|
51
|
+
/** The server's version arrived. */
|
|
52
|
+
applyServer(record, status = "fresh") {
|
|
53
|
+
batch(() => {
|
|
54
|
+
const keys = new Set([...Object.keys(this.baseline), ...Object.keys(record)]);
|
|
55
|
+
this.baseline = { ...record };
|
|
56
|
+
for (const key of keys)
|
|
57
|
+
this.refresh(key);
|
|
58
|
+
if (this.pending.size === 0)
|
|
59
|
+
this.status.set(status);
|
|
60
|
+
this.presence.set("loaded");
|
|
61
|
+
this.loadError.set(null);
|
|
62
|
+
this.reload = false;
|
|
63
|
+
this.version.set(this.version.peek() + 1);
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
setPending(patch) {
|
|
67
|
+
batch(() => {
|
|
68
|
+
for (const [k, v] of Object.entries(patch)) {
|
|
69
|
+
this.pending.set(k, v);
|
|
70
|
+
this.refresh(k);
|
|
71
|
+
}
|
|
72
|
+
this.status.set("pending");
|
|
73
|
+
this.version.set(this.version.peek() + 1);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** Drop pending edits for `keys` (confirmed or rejected). */
|
|
77
|
+
settle(keys, status, errors = {}) {
|
|
78
|
+
batch(() => {
|
|
79
|
+
for (const k of keys) {
|
|
80
|
+
this.pending.delete(k);
|
|
81
|
+
this.refresh(k);
|
|
82
|
+
}
|
|
83
|
+
if (this.pending.size === 0)
|
|
84
|
+
this.status.set(status);
|
|
85
|
+
this.errors.set(errors);
|
|
86
|
+
this.version.set(this.version.peek() + 1);
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
/** The server refused to load it. */
|
|
90
|
+
refused(message) {
|
|
91
|
+
batch(() => {
|
|
92
|
+
this.loadError.set(message);
|
|
93
|
+
this.presence.set("error");
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
markRemoved() {
|
|
97
|
+
batch(() => {
|
|
98
|
+
this.baseline = {};
|
|
99
|
+
this.pending.clear();
|
|
100
|
+
for (const key of this.fields.keys())
|
|
101
|
+
this.refresh(key);
|
|
102
|
+
this.presence.set("missing");
|
|
103
|
+
this.version.set(this.version.peek() + 1);
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
effective(name) {
|
|
107
|
+
if (name === "id")
|
|
108
|
+
return this.id;
|
|
109
|
+
return this.pending.has(name) ? this.pending.get(name) : (this.baseline[name] ?? null);
|
|
110
|
+
}
|
|
111
|
+
refresh(name) {
|
|
112
|
+
const state = this.fields.get(name);
|
|
113
|
+
if (state)
|
|
114
|
+
state.set(this.effective(name));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
export class Store {
|
|
118
|
+
tables = new Map();
|
|
119
|
+
cell(table, id) {
|
|
120
|
+
let cells = this.tables.get(table);
|
|
121
|
+
if (!cells) {
|
|
122
|
+
cells = new Map();
|
|
123
|
+
this.tables.set(table, cells);
|
|
124
|
+
}
|
|
125
|
+
let cell = cells.get(id);
|
|
126
|
+
if (!cell) {
|
|
127
|
+
cell = new RecordCell(table, id);
|
|
128
|
+
cells.set(id, cell);
|
|
129
|
+
}
|
|
130
|
+
return cell;
|
|
131
|
+
}
|
|
132
|
+
peek(table, id) {
|
|
133
|
+
return this.tables.get(table)?.get(id);
|
|
134
|
+
}
|
|
135
|
+
cells(table) {
|
|
136
|
+
return (this.tables.get(table) ?? new Map()).values();
|
|
137
|
+
}
|
|
138
|
+
allCells() {
|
|
139
|
+
return [...this.tables.values()].flatMap((m) => [...m.values()]);
|
|
140
|
+
}
|
|
141
|
+
evict(table, id) {
|
|
142
|
+
this.tables.get(table)?.delete(id);
|
|
143
|
+
}
|
|
144
|
+
}
|
package/dist/table.d.ts
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import type { Vantage } from "./client.js";
|
|
2
|
+
import type { AnyModel } from "./model.js";
|
|
3
|
+
import type { Where } from "./protocol.js";
|
|
4
|
+
import type { RunFactory, RunTypes } from "./run/types.js";
|
|
5
|
+
import type { ActionResult, LazyCollection, LazyReference, SchemaTypes, TableMeta } from "./types.js";
|
|
6
|
+
type ModelsOf<S extends SchemaTypes> = S["models"];
|
|
7
|
+
export type TableName<S extends SchemaTypes> = keyof ModelsOf<S> & string;
|
|
8
|
+
export type ModelOf<S extends SchemaTypes, T> = T extends keyof ModelsOf<S> ? ModelsOf<S>[T] : never;
|
|
9
|
+
export type InputOf<S extends SchemaTypes, T> = T extends keyof S["inputs"] ? S["inputs"][T] : never;
|
|
10
|
+
type ArgsFor<I> = {} extends I ? [input?: I] : [input: I];
|
|
11
|
+
export type TableActionsOf<S extends SchemaTypes, T> = T extends keyof S["tableActions"] ? {
|
|
12
|
+
[A in keyof S["tableActions"][T]]: (...input: ArgsFor<S["tableActions"][T][A]>) => Promise<ActionResult<undefined>>;
|
|
13
|
+
} : {};
|
|
14
|
+
/** `db.run`: a run factory per project action. */
|
|
15
|
+
export type ProjectActions<S extends SchemaTypes> = {
|
|
16
|
+
[A in keyof S["projectActions"]]: RunFactory<S["projectActions"][A] extends RunTypes ? S["projectActions"][A] : RunTypes>;
|
|
17
|
+
};
|
|
18
|
+
export interface TableHandle<M, I, A> {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly meta: TableMeta;
|
|
21
|
+
/** The whole table (as far as your scope goes). */
|
|
22
|
+
readonly all: LazyCollection<M>;
|
|
23
|
+
/** The record if it's loaded; reading starts loading it. */
|
|
24
|
+
get(id: string): M | undefined;
|
|
25
|
+
ref(id: string): LazyReference<M>;
|
|
26
|
+
where(conditions: Partial<Record<string, unknown>> | Where[]): LazyCollection<M>;
|
|
27
|
+
sort(column: string, direction?: "asc" | "desc"): LazyCollection<M>;
|
|
28
|
+
search(text: string): LazyCollection<M>;
|
|
29
|
+
create(input: I): Promise<ActionResult<M>>;
|
|
30
|
+
/** Toolbar actions of the table's pages. */
|
|
31
|
+
readonly actions: A;
|
|
32
|
+
}
|
|
33
|
+
export type Tables<S extends SchemaTypes> = {
|
|
34
|
+
readonly [T in TableName<S>]: TableHandle<ModelOf<S, T>, InputOf<S, T>, TableActionsOf<S, T>>;
|
|
35
|
+
};
|
|
36
|
+
export declare class Table implements TableHandle<AnyModel, Record<string, unknown>, Record<string, unknown>> {
|
|
37
|
+
private db;
|
|
38
|
+
readonly name: string;
|
|
39
|
+
readonly meta: TableMeta;
|
|
40
|
+
readonly actions: Record<string, unknown>;
|
|
41
|
+
constructor(db: Vantage<any>, name: string, meta: TableMeta);
|
|
42
|
+
get all(): LazyCollection<AnyModel>;
|
|
43
|
+
get(id: string): AnyModel | undefined;
|
|
44
|
+
ref(id: string): LazyReference<AnyModel>;
|
|
45
|
+
where(conditions: Partial<Record<string, unknown>> | Where[]): LazyCollection<AnyModel>;
|
|
46
|
+
sort(column: string, direction?: "asc" | "desc"): LazyCollection<AnyModel>;
|
|
47
|
+
search(text: string): LazyCollection<AnyModel>;
|
|
48
|
+
create(input: Record<string, unknown>): Promise<ActionResult<AnyModel>>;
|
|
49
|
+
}
|
|
50
|
+
export declare class Reference implements LazyReference<AnyModel> {
|
|
51
|
+
private db;
|
|
52
|
+
private table;
|
|
53
|
+
private idFn;
|
|
54
|
+
constructor(db: Vantage<any>, table: string, idFn: () => string | null);
|
|
55
|
+
get id(): string | null;
|
|
56
|
+
get value(): AnyModel | undefined;
|
|
57
|
+
get status(): import("./store.js").Presence;
|
|
58
|
+
get error(): string | null;
|
|
59
|
+
load(): Promise<AnyModel | undefined>;
|
|
60
|
+
then<A = AnyModel | undefined, B = never>(onfulfilled?: ((value: AnyModel | undefined) => A | PromiseLike<A>) | null, onrejected?: ((reason: unknown) => B | PromiseLike<B>) | null): PromiseLike<A | B>;
|
|
61
|
+
}
|
|
62
|
+
export {};
|
package/dist/table.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// The typed table API: `db.order.where(…)`, `db.order.ref(id)`, and the
|
|
2
|
+
// references that point at one record.
|
|
3
|
+
import { toColumns } from "./util.js";
|
|
4
|
+
export class Table {
|
|
5
|
+
db;
|
|
6
|
+
name;
|
|
7
|
+
meta;
|
|
8
|
+
actions;
|
|
9
|
+
constructor(db, name, meta) {
|
|
10
|
+
this.db = db;
|
|
11
|
+
this.name = name;
|
|
12
|
+
this.meta = meta;
|
|
13
|
+
this.actions = new Proxy({}, { get: (_, action) => (input) => db.invoke(`${name}.${action}`, undefined, input ?? {}) });
|
|
14
|
+
}
|
|
15
|
+
get all() {
|
|
16
|
+
return this.db.collection({ table: this.name });
|
|
17
|
+
}
|
|
18
|
+
get(id) {
|
|
19
|
+
return this.ref(id).value;
|
|
20
|
+
}
|
|
21
|
+
ref(id) {
|
|
22
|
+
return this.db.ref(this.name, () => id);
|
|
23
|
+
}
|
|
24
|
+
where(conditions) {
|
|
25
|
+
return this.all.where(conditions);
|
|
26
|
+
}
|
|
27
|
+
sort(column, direction = "asc") {
|
|
28
|
+
return this.all.sort(column, direction);
|
|
29
|
+
}
|
|
30
|
+
search(text) {
|
|
31
|
+
return this.all.search(text);
|
|
32
|
+
}
|
|
33
|
+
create(input) {
|
|
34
|
+
return this.db.write(this.name, undefined, toColumns(this.meta, input));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
export class Reference {
|
|
38
|
+
db;
|
|
39
|
+
table;
|
|
40
|
+
idFn;
|
|
41
|
+
constructor(db, table, idFn) {
|
|
42
|
+
this.db = db;
|
|
43
|
+
this.table = table;
|
|
44
|
+
this.idFn = idFn;
|
|
45
|
+
}
|
|
46
|
+
get id() {
|
|
47
|
+
return this.idFn();
|
|
48
|
+
}
|
|
49
|
+
get value() {
|
|
50
|
+
const id = this.id;
|
|
51
|
+
if (!id)
|
|
52
|
+
return undefined;
|
|
53
|
+
const cell = this.db.store.cell(this.table, id);
|
|
54
|
+
cell.touched = Date.now();
|
|
55
|
+
const presence = cell.presence.get();
|
|
56
|
+
if (presence === "none")
|
|
57
|
+
void this.db.requestRecords(this.table, [id]);
|
|
58
|
+
if (presence === "loaded" && cell.reload) {
|
|
59
|
+
cell.reload = false;
|
|
60
|
+
void this.db.reloadRecords(this.table, [id]);
|
|
61
|
+
}
|
|
62
|
+
return presence === "loaded" ? this.db.model(this.table, id) : undefined;
|
|
63
|
+
}
|
|
64
|
+
get status() {
|
|
65
|
+
const id = this.id;
|
|
66
|
+
return id ? this.db.store.cell(this.table, id).presence.get() : "none";
|
|
67
|
+
}
|
|
68
|
+
get error() {
|
|
69
|
+
const id = this.id;
|
|
70
|
+
return id ? this.db.store.cell(this.table, id).loadError.get() : null;
|
|
71
|
+
}
|
|
72
|
+
async load() {
|
|
73
|
+
const id = this.id;
|
|
74
|
+
if (!id)
|
|
75
|
+
return undefined;
|
|
76
|
+
await this.db.requestRecords(this.table, [id]);
|
|
77
|
+
return this.db.store.cell(this.table, id).presence.peek() === "loaded" ? this.db.model(this.table, id) : undefined;
|
|
78
|
+
}
|
|
79
|
+
then(onfulfilled, onrejected) {
|
|
80
|
+
return this.load().then(onfulfilled, onrejected);
|
|
81
|
+
}
|
|
82
|
+
}
|