@vsceasy/cli 0.1.7 → 0.1.9
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 +10 -0
- package/README.md +10 -0
- package/dist/bin/cli.js +543 -72
- package/dist/commands/groups.d.ts +1 -0
- package/dist/commands/store/add.d.ts +3 -0
- package/dist/index.js +327 -26
- package/dist/lib/crud/parseModel.d.ts +12 -0
- package/dist/lib/model/add.d.ts +18 -0
- package/dist/lib/model/parseFields.d.ts +9 -3
- package/dist/lib/store/add.d.ts +14 -0
- package/dist/lib/templatesData.d.ts +1 -1
- package/package.json +1 -1
- package/templates/_generators/crud/formApp.tsx.tpl +2 -0
- package/templates/_generators/crud/formPanel.ts.tpl +2 -2
- package/templates/_generators/helper/db.ts.tpl +35 -1
- package/templates/_generators/model/model.ts.tpl +1 -1
- package/templates/_generators/store/store.ts.tpl +23 -0
- package/templates/react/src/shared/vsceasy/bootstrap.ts +19 -6
- package/templates/react/src/shared/vsceasy/client.ts +27 -0
- package/templates/react/src/shared/vsceasy/define.ts +34 -4
- package/templates/react/src/shared/vsceasy/index.ts +4 -1
- package/templates/react/src/shared/vsceasy/store.ts +73 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ModelField } from './add';
|
|
1
|
+
import type { ModelField, FieldRelation } from './add';
|
|
2
2
|
/**
|
|
3
3
|
* Parse a compact model field spec into `ModelField[]`.
|
|
4
4
|
*
|
|
@@ -7,8 +7,14 @@ import type { ModelField } from './add';
|
|
|
7
7
|
* `!` after type → primaryKey
|
|
8
8
|
* `@` after type → indexed
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* Relations use `name:ref(Model)` or `name:ref(Model, label=field)`:
|
|
11
|
+
* category:ref(Category) → FK categoryId, dropdown of Category rows
|
|
12
|
+
* category:ref(Category, label=name) → show Category.name in the dropdown
|
|
13
|
+
*
|
|
14
|
+
* Example: `id:string!,name:string,email?:string@,category:ref(Category)`
|
|
11
15
|
*/
|
|
12
16
|
export declare function parseFieldsSpec(spec: string): ModelField[];
|
|
13
|
-
/** Parse a single `name[?]:type[!][@]` line. Throws on malformed input. */
|
|
17
|
+
/** Parse a single `name[?]:type[!][@]` line (or `name:ref(Model)`). Throws on malformed input. */
|
|
14
18
|
export declare function parseFieldLine(raw: string): ModelField;
|
|
19
|
+
/** Parse `ref(Model)` / `ref(Model, label=field)`. Returns null when not a ref. */
|
|
20
|
+
export declare function parseRef(type: string): FieldRelation | null;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
export type StoreType = 'number' | 'string' | 'boolean' | 'json';
|
|
2
|
+
export interface AddStoreOptions {
|
|
3
|
+
name: string;
|
|
4
|
+
/** Value type. Default: 'number'. */
|
|
5
|
+
type?: StoreType;
|
|
6
|
+
/** Raw initial value expression. Defaults per type (0 / '' / false / null). */
|
|
7
|
+
initial?: string;
|
|
8
|
+
projectRoot: string;
|
|
9
|
+
templatesRoot: string;
|
|
10
|
+
}
|
|
11
|
+
export interface AddStoreResult {
|
|
12
|
+
created: string[];
|
|
13
|
+
}
|
|
14
|
+
export declare function addStore(opts: AddStoreOptions): AddStoreResult;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export declare const TEMPLATES_VERSION = "0.1.
|
|
1
|
+
export declare const TEMPLATES_VERSION = "0.1.9";
|
|
2
2
|
export declare const TEMPLATE_FILES: Record<string, string>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@vsceasy/cli",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.9",
|
|
4
4
|
"description": "Build VS Code extensions fast — React UI + typed RPC bridge between extension and webview + file-based routing for panels, commands, menus, tree views, and subpanels.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
@@ -22,6 +22,7 @@ export function App() {
|
|
|
22
22
|
const [editingId, setEditingId] = useState<{{Name}}['{{primaryKey}}'] | null>(null);
|
|
23
23
|
const [error, setError] = useState<string | null>(null);
|
|
24
24
|
const [saving, setSaving] = useState(false);
|
|
25
|
+
{{relationOptionsState}}
|
|
25
26
|
|
|
26
27
|
const load = useCallback(async (initial: boolean) => {
|
|
27
28
|
// The list stashes a row id before revealing this panel. Pull it (the host
|
|
@@ -59,6 +60,7 @@ export function App() {
|
|
|
59
60
|
document.removeEventListener('visibilitychange', onVisible);
|
|
60
61
|
};
|
|
61
62
|
}, [load]);
|
|
63
|
+
{{relationOptionsLoad}}
|
|
62
64
|
|
|
63
65
|
const onChange = <K extends keyof FormState>(k: K, v: FormState[K]) => {
|
|
64
66
|
setForm((f) => ({ ...f, [k]: v }));
|
|
@@ -3,7 +3,7 @@ import { {{Name}}Service } from '../services/{{Name}}Service';
|
|
|
3
3
|
import { takePending{{Name}}Id } from '../services/{{name}}FormNav';
|
|
4
4
|
import type { {{Name}}FormApi } from '../shared/api';
|
|
5
5
|
import type { {{Name}} } from '../models/{{Name}}';
|
|
6
|
-
|
|
6
|
+
{{relationImports}}
|
|
7
7
|
export default definePanel<{{Name}}FormApi>({
|
|
8
8
|
title: '{{title}}',
|
|
9
9
|
column: 'beside',
|
|
@@ -25,7 +25,7 @@ export default definePanel<{{Name}}FormApi>({
|
|
|
25
25
|
void vscode.commands.executeCommand('{{prefix}}.open{{Plural}}List');
|
|
26
26
|
return saved;
|
|
27
27
|
},
|
|
28
|
-
async cancel() {
|
|
28
|
+
{{relationOptionsHandler}} async cancel() {
|
|
29
29
|
// No-op — webview closes itself.
|
|
30
30
|
},
|
|
31
31
|
}),
|
|
@@ -64,6 +64,35 @@ export interface Repository<T> {
|
|
|
64
64
|
clear(): Promise<void>;
|
|
65
65
|
}
|
|
66
66
|
|
|
67
|
+
// ── Change notifications (reactivity) ────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Per-entity change listeners. Any mutation (insert/upsert/update/delete/...) on
|
|
71
|
+
* an entity fires its listeners with the entity name. This is the host-side hook
|
|
72
|
+
* the `watch()` helper builds on to push updates to webviews.
|
|
73
|
+
*/
|
|
74
|
+
const entityListeners = new Map<string, Set<(entity: string) => void>>();
|
|
75
|
+
|
|
76
|
+
function emitChange(entity: string): void {
|
|
77
|
+
entityListeners.get(entity)?.forEach((cb) => {
|
|
78
|
+
try { cb(entity); } catch { /* a bad listener must not break a write */ }
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Subscribe to changes on an entity. Returns an unsubscribe function.
|
|
84
|
+
*
|
|
85
|
+
* watchEntity(Todos, () => server.emit('todos:changed'));
|
|
86
|
+
*
|
|
87
|
+
* Prefer the higher-level `watch()` helper, which wires this to an RPC emit.
|
|
88
|
+
*/
|
|
89
|
+
export function watchEntity<T>(entity: Entity<T>, cb: (entity: string) => void): () => void {
|
|
90
|
+
let set = entityListeners.get(entity.name);
|
|
91
|
+
if (!set) entityListeners.set(entity.name, (set = new Set()));
|
|
92
|
+
set.add(cb);
|
|
93
|
+
return () => { set!.delete(cb); };
|
|
94
|
+
}
|
|
95
|
+
|
|
67
96
|
// ── Provider interface (future-proof for sqlite/etc.) ────────────────────────
|
|
68
97
|
|
|
69
98
|
export interface Provider {
|
|
@@ -234,6 +263,7 @@ function makeDb(provider: Provider): Db {
|
|
|
234
263
|
}
|
|
235
264
|
rows.push(row);
|
|
236
265
|
await save(rows);
|
|
266
|
+
emitChange(entity.name);
|
|
237
267
|
return row;
|
|
238
268
|
},
|
|
239
269
|
async upsert(row) {
|
|
@@ -241,6 +271,7 @@ function makeDb(provider: Provider): Db {
|
|
|
241
271
|
const i = rows.findIndex((r) => r[pk] === row[pk]);
|
|
242
272
|
if (i >= 0) rows[i] = row; else rows.push(row);
|
|
243
273
|
await save(rows);
|
|
274
|
+
emitChange(entity.name);
|
|
244
275
|
return row;
|
|
245
276
|
},
|
|
246
277
|
async update(id, patch) {
|
|
@@ -249,6 +280,7 @@ function makeDb(provider: Provider): Db {
|
|
|
249
280
|
if (i < 0) return null;
|
|
250
281
|
rows[i] = { ...rows[i], ...patch };
|
|
251
282
|
await save(rows);
|
|
283
|
+
emitChange(entity.name);
|
|
252
284
|
return rows[i];
|
|
253
285
|
},
|
|
254
286
|
async delete(id) {
|
|
@@ -257,17 +289,19 @@ function makeDb(provider: Provider): Db {
|
|
|
257
289
|
const next = rows.filter((r) => r[pk] !== id);
|
|
258
290
|
if (next.length === before) return false;
|
|
259
291
|
await save(next);
|
|
292
|
+
emitChange(entity.name);
|
|
260
293
|
return true;
|
|
261
294
|
},
|
|
262
295
|
async deleteMany(where) {
|
|
263
296
|
const rows = await load();
|
|
264
297
|
const next = rows.filter((r) => !match(r, where));
|
|
265
298
|
const removed = rows.length - next.length;
|
|
266
|
-
if (removed > 0) await save(next);
|
|
299
|
+
if (removed > 0) { await save(next); emitChange(entity.name); }
|
|
267
300
|
return removed;
|
|
268
301
|
},
|
|
269
302
|
async clear() {
|
|
270
303
|
await save([]);
|
|
304
|
+
emitChange(entity.name);
|
|
271
305
|
},
|
|
272
306
|
};
|
|
273
307
|
};
|
|
@@ -14,4 +14,4 @@ export const {{Plural}} = defineEntity<{{Name}}>('{{collection}}', {
|
|
|
14
14
|
* import { {{Plural}}Repo } from '../models/{{Name}}';
|
|
15
15
|
* await {{Plural}}Repo().insert({ ... });
|
|
16
16
|
*/
|
|
17
|
-
export const {{Plural}}Repo = () => db()({{Plural}});
|
|
17
|
+
export const {{Plural}}Repo = () => db()({{Plural}});{{relationsBlock}}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { defineStore } from '../shared/vsceasy';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Reactive store: a single observable value. Mutate it with `.set()` / `.update()`
|
|
5
|
+
* and anything subscribed reacts.
|
|
6
|
+
*
|
|
7
|
+
* import { {{name}} } from '../stores/{{name}}';
|
|
8
|
+
* {{name}}.set({{example}});
|
|
9
|
+
* {{name}}.update((v) => v);
|
|
10
|
+
*
|
|
11
|
+
* To push changes to a webview, watch it on the host and emit over RPC:
|
|
12
|
+
*
|
|
13
|
+
* // inside a panel/subpanel rpc():
|
|
14
|
+
* import { watch } from '../shared/vsceasy';
|
|
15
|
+
* import { {{name}} } from '../stores/{{name}}';
|
|
16
|
+
* watch({{name}}, () => server.emit('{{name}}:changed', {{name}}.get()));
|
|
17
|
+
*
|
|
18
|
+
* Then listen in the webview:
|
|
19
|
+
*
|
|
20
|
+
* import { listen } from '../shared/vsceasy/client';
|
|
21
|
+
* listen(api, '{{name}}:changed', (v) => render(v));
|
|
22
|
+
*/
|
|
23
|
+
export const {{name}} = defineStore<{{type}}>({{initial}});
|
|
@@ -230,6 +230,13 @@ function registerTreeView(
|
|
|
230
230
|
vscode.commands.registerCommand(refreshCmd, () => provider.refresh()),
|
|
231
231
|
);
|
|
232
232
|
|
|
233
|
+
// Keep the tree live: let the def subscribe to a data source and re-run
|
|
234
|
+
// getChildren on change. The unsubscribe is disposed on deactivate.
|
|
235
|
+
if (def.watch) {
|
|
236
|
+
const off = def.watch(() => provider.refresh(), vscode, context);
|
|
237
|
+
if (off) context.subscriptions.push({ dispose: off });
|
|
238
|
+
}
|
|
239
|
+
|
|
233
240
|
const dispatchCmd = `${registry.prefix}._tree.${def.id ?? id}.run`;
|
|
234
241
|
context.subscriptions.push(
|
|
235
242
|
vscode.commands.registerCommand(dispatchCmd, async (node: TreeNode) => {
|
|
@@ -306,9 +313,11 @@ function registerSubpanel(
|
|
|
306
313
|
const ui = def.ui ?? `subpanels/${def.id ?? id}`;
|
|
307
314
|
view.webview.html = renderHtml(view.webview, context, ui, def.title);
|
|
308
315
|
if (def.rpc) {
|
|
309
|
-
|
|
310
|
-
const
|
|
311
|
-
|
|
316
|
+
let server: ReturnType<typeof createRpcServer> | undefined;
|
|
317
|
+
const emit = (topic: string, payload?: unknown) => server?.emit(topic, payload);
|
|
318
|
+
const handlers = def.rpc(vscode, context, emit);
|
|
319
|
+
server = createRpcServer(webviewTransport(view.webview), handlers);
|
|
320
|
+
view.onDidDispose(() => server!.dispose());
|
|
312
321
|
}
|
|
313
322
|
},
|
|
314
323
|
};
|
|
@@ -544,9 +553,13 @@ function openPanel(context: vscode.ExtensionContext, prefix: string, id: string,
|
|
|
544
553
|
panel.webview.html = renderHtml(panel.webview, context, ui, def.title);
|
|
545
554
|
|
|
546
555
|
if (def.rpc) {
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
556
|
+
// `emit` is wired lazily: the rpc factory may call watch()/watchEntity()
|
|
557
|
+
// immediately, but the server (and thus the real emit) only exists after.
|
|
558
|
+
let server: ReturnType<typeof createRpcServer> | undefined;
|
|
559
|
+
const emit = (topic: string, payload?: unknown) => server?.emit(topic, payload);
|
|
560
|
+
const handlers = def.rpc(vscode, context, emit);
|
|
561
|
+
server = createRpcServer(webviewTransport(panel.webview), handlers);
|
|
562
|
+
panel.onDidDispose(() => server!.dispose());
|
|
550
563
|
}
|
|
551
564
|
|
|
552
565
|
openPanels.set(key, panel);
|
|
@@ -6,3 +6,30 @@ export {
|
|
|
6
6
|
webviewState,
|
|
7
7
|
} from './rpc';
|
|
8
8
|
export type { RpcClient, Handlers, Transport, RpcClientOptions, WebviewApi } from './rpc';
|
|
9
|
+
|
|
10
|
+
export { defineStore } from './store';
|
|
11
|
+
export type { Store } from './store';
|
|
12
|
+
|
|
13
|
+
import type { RpcClient, Handlers } from './rpc';
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Listen for a change pushed from the host over the RPC event channel, and run a
|
|
17
|
+
* callback when it arrives. This is the webview side of the reactivity model —
|
|
18
|
+
* the host emits with `server.emit(topic, …)` (usually wired via `watch()` /
|
|
19
|
+
* `watchEntity()`), and your visual element reacts here. Returns an unsubscribe
|
|
20
|
+
* function; call it on unmount.
|
|
21
|
+
*
|
|
22
|
+
* const api = connectWebview<TodoStatsViewApi>();
|
|
23
|
+
* // re-read + re-render whenever todos change on the host:
|
|
24
|
+
* const off = listen(api, 'todos:changed', () => refresh());
|
|
25
|
+
*
|
|
26
|
+
* It's a thin, named wrapper over `api.on(topic, handler)` so the place you
|
|
27
|
+
* listen reads clearly in the UI code. The payload (if any) is passed through.
|
|
28
|
+
*/
|
|
29
|
+
export function listen<H extends Handlers>(
|
|
30
|
+
api: RpcClient<H>,
|
|
31
|
+
topic: string,
|
|
32
|
+
handler: (payload?: unknown) => void,
|
|
33
|
+
): () => void {
|
|
34
|
+
return api.on(topic, handler);
|
|
35
|
+
}
|
|
@@ -2,6 +2,18 @@ import type * as vscode from 'vscode';
|
|
|
2
2
|
import type { Handlers } from './rpc';
|
|
3
3
|
import type { CodiconName } from './codiconNames';
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* Push a change to this webview over the RPC event channel. The webview reacts
|
|
7
|
+
* with `listen(api, topic, …)`. Wire it to a data source with `watch()` /
|
|
8
|
+
* `watchEntity()` inside `rpc()`:
|
|
9
|
+
*
|
|
10
|
+
* rpc: (vscode, ctx, emit) => {
|
|
11
|
+
* watchEntity(Todos, () => emit('todos:changed'));
|
|
12
|
+
* return { stats: () => … };
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
export type RpcEmit = (topic: string, payload?: unknown) => void;
|
|
16
|
+
|
|
5
17
|
export interface PanelDef<H extends Handlers = Handlers> {
|
|
6
18
|
/** Stable id. Default: file basename. Used as command suffix and webview key. */
|
|
7
19
|
id?: string;
|
|
@@ -13,8 +25,11 @@ export interface PanelDef<H extends Handlers = Handlers> {
|
|
|
13
25
|
column?: 'active' | 'beside' | 'one' | 'two' | 'three';
|
|
14
26
|
/** Keep DOM alive when hidden. Default: true. */
|
|
15
27
|
retainContext?: boolean;
|
|
16
|
-
/**
|
|
17
|
-
|
|
28
|
+
/**
|
|
29
|
+
* RPC handlers — receives the vscode namespace, the extension context, and
|
|
30
|
+
* `emit` for pushing change events to this webview (see {@link RpcEmit}).
|
|
31
|
+
*/
|
|
32
|
+
rpc?: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext, emit: RpcEmit) => H;
|
|
18
33
|
/** Optional command palette entry that opens this panel. Default: true. */
|
|
19
34
|
command?:
|
|
20
35
|
| boolean
|
|
@@ -125,8 +140,11 @@ export interface SubpanelDef<H extends Handlers = Handlers> {
|
|
|
125
140
|
ui?: string;
|
|
126
141
|
/** Keep DOM alive when hidden. Default: true. */
|
|
127
142
|
retainContext?: boolean;
|
|
128
|
-
/**
|
|
129
|
-
|
|
143
|
+
/**
|
|
144
|
+
* RPC handlers — receives the vscode namespace, the extension context, and
|
|
145
|
+
* `emit` for pushing change events to this webview (see {@link RpcEmit}).
|
|
146
|
+
*/
|
|
147
|
+
rpc?: (vscode: typeof import('vscode'), ctx: vscode.ExtensionContext, emit: RpcEmit) => H;
|
|
130
148
|
}
|
|
131
149
|
|
|
132
150
|
export function defineSubpanel<H extends Handlers = Handlers>(def: SubpanelDef<H>): SubpanelDef<H> {
|
|
@@ -228,6 +246,18 @@ export interface TreeViewDef {
|
|
|
228
246
|
vscode: typeof import('vscode'),
|
|
229
247
|
ctx: vscode.ExtensionContext,
|
|
230
248
|
) => TreeNode[] | Promise<TreeNode[]>;
|
|
249
|
+
/**
|
|
250
|
+
* Keep the tree live. Receives `refresh` — call it (directly or as a callback)
|
|
251
|
+
* to re-run `getChildren`. Subscribe to a data source here so the tree updates
|
|
252
|
+
* itself; return an unsubscribe to clean up.
|
|
253
|
+
*
|
|
254
|
+
* watch: (refresh) => watchEntity(Todos, refresh),
|
|
255
|
+
*/
|
|
256
|
+
watch?: (
|
|
257
|
+
refresh: () => void,
|
|
258
|
+
vscode: typeof import('vscode'),
|
|
259
|
+
ctx: vscode.ExtensionContext,
|
|
260
|
+
) => (() => void) | void;
|
|
231
261
|
}
|
|
232
262
|
|
|
233
263
|
export function defineTreeView(def: TreeViewDef): TreeViewDef {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
export { definePanel, defineCommand, defineMenu, defineStatusBar, defineSubpanel, defineTreeView, defineJob } from './define';
|
|
2
|
-
export type { PanelDef, CommandDef, MenuDef, MenuItem, MenuIcon, StatusBarDef, StatusBarMenuItem, KeybindingDef, SubpanelDef, TreeViewDef, TreeNode, JobDef, JobSchedule, CodiconName } from './define';
|
|
2
|
+
export type { PanelDef, CommandDef, MenuDef, MenuItem, MenuIcon, StatusBarDef, StatusBarMenuItem, KeybindingDef, SubpanelDef, TreeViewDef, TreeNode, JobDef, JobSchedule, CodiconName, RpcEmit } from './define';
|
|
3
3
|
export { bootstrap } from './bootstrap';
|
|
4
4
|
export type { Registry, BootstrapOptions, ActivateHook } from './bootstrap';
|
|
5
5
|
export {
|
|
@@ -11,3 +11,6 @@ export {
|
|
|
11
11
|
webviewState,
|
|
12
12
|
} from './rpc';
|
|
13
13
|
export type { Transport, RpcClient, Handlers, RpcClientOptions, WebviewApi } from './rpc';
|
|
14
|
+
export { defineStore, watch } from './store';
|
|
15
|
+
export type { Store, Watchable } from './store';
|
|
16
|
+
export { listen } from './client';
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactive stores — a tiny, framework-agnostic observable value.
|
|
3
|
+
*
|
|
4
|
+
* A store holds one value and notifies subscribers when it changes. It's the
|
|
5
|
+
* non-ORM half of the reactivity model: use it for arbitrary state (a counter,
|
|
6
|
+
* a flag, a selection) that a visual element should track.
|
|
7
|
+
*
|
|
8
|
+
* const counter = defineStore(0);
|
|
9
|
+
* counter.subscribe((v) => console.log('now', v));
|
|
10
|
+
* counter.set(1); // logs: now 1
|
|
11
|
+
* counter.update((n) => n + 1);
|
|
12
|
+
*
|
|
13
|
+
* Pair it with `watch()` on the host to push changes to a webview, and
|
|
14
|
+
* `listen()` on the webview to react — see those helpers below and in client.ts.
|
|
15
|
+
*/
|
|
16
|
+
export interface Store<T> {
|
|
17
|
+
/** Read the current value. */
|
|
18
|
+
get(): T;
|
|
19
|
+
/** Replace the value and notify subscribers (no-op if `Object.is`-equal). */
|
|
20
|
+
set(next: T): void;
|
|
21
|
+
/** Derive the next value from the current one, then `set` it. */
|
|
22
|
+
update(fn: (current: T) => T): void;
|
|
23
|
+
/** Subscribe to changes. Returns an unsubscribe function. */
|
|
24
|
+
subscribe(cb: (value: T) => void): () => void;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function defineStore<T>(initial: T): Store<T> {
|
|
28
|
+
let value = initial;
|
|
29
|
+
const subs = new Set<(value: T) => void>();
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
get: () => value,
|
|
33
|
+
set(next) {
|
|
34
|
+
if (Object.is(value, next)) return;
|
|
35
|
+
value = next;
|
|
36
|
+
subs.forEach((cb) => {
|
|
37
|
+
try { cb(value); } catch { /* a bad subscriber must not break a set */ }
|
|
38
|
+
});
|
|
39
|
+
},
|
|
40
|
+
update(fn) {
|
|
41
|
+
this.set(fn(value));
|
|
42
|
+
},
|
|
43
|
+
subscribe(cb) {
|
|
44
|
+
subs.add(cb);
|
|
45
|
+
return () => { subs.delete(cb); };
|
|
46
|
+
},
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Anything that can be watched: it exposes a `subscribe(cb)` returning an
|
|
52
|
+
* unsubscribe function. Stores satisfy this directly. (ORM entities are watched
|
|
53
|
+
* with `watchEntity` from your generated `db.ts`, which has the same shape.)
|
|
54
|
+
*/
|
|
55
|
+
export interface Watchable {
|
|
56
|
+
subscribe(cb: (...args: any[]) => void): () => void;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Bridge a watchable source to a side-effect — typically an RPC emit that pushes
|
|
61
|
+
* the change to a subscribed webview. Runs `effect` on every change. Returns an
|
|
62
|
+
* unsubscribe function; register it on the panel's `ctx.subscriptions` (wrapped
|
|
63
|
+
* in `{ dispose }`) so it's cleaned up when the extension deactivates.
|
|
64
|
+
*
|
|
65
|
+
* // host side, in a panel's rpc():
|
|
66
|
+
* watch(badgeCount, () => server.emit('badge:changed'));
|
|
67
|
+
*
|
|
68
|
+
* For ORM entities, use `watchEntity(Todos, () => server.emit('todos:changed'))`
|
|
69
|
+
* from your generated db.ts — same idea, same return.
|
|
70
|
+
*/
|
|
71
|
+
export function watch(source: Watchable, effect: () => void): () => void {
|
|
72
|
+
return source.subscribe(() => effect());
|
|
73
|
+
}
|