@treenity/core 3.0.0 → 3.0.1
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 +78 -0
- package/dist/chain.d.ts +3 -4
- package/dist/chain.d.ts.map +1 -1
- package/dist/chain.js.map +1 -1
- package/dist/client/trpc.d.ts.map +1 -1
- package/dist/client/trpc.js +1 -0
- package/dist/client/trpc.js.map +1 -1
- package/dist/comp/index.d.ts +3 -4
- package/dist/comp/index.d.ts.map +1 -1
- package/dist/comp/index.js +5 -4
- package/dist/comp/index.js.map +1 -1
- package/dist/comp/needs.d.ts.map +1 -1
- package/dist/comp/needs.js +3 -3
- package/dist/comp/needs.js.map +1 -1
- package/dist/core/component.d.ts +10 -8
- package/dist/core/component.d.ts.map +1 -1
- package/dist/core/component.js +4 -8
- package/dist/core/component.js.map +1 -1
- package/dist/core/context.d.ts +2 -2
- package/dist/core/context.d.ts.map +1 -1
- package/dist/core/path.d.ts.map +1 -1
- package/dist/core/path.js +7 -3
- package/dist/core/path.js.map +1 -1
- package/dist/core/registry.d.ts +2 -1
- package/dist/core/registry.d.ts.map +1 -1
- package/dist/core/registry.js.map +1 -1
- package/dist/core.d.ts +1 -1
- package/dist/core.d.ts.map +1 -1
- package/dist/core.js +1 -1
- package/dist/core.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -0
- package/dist/log.d.ts +30 -0
- package/dist/log.d.ts.map +1 -1
- package/dist/log.js +119 -0
- package/dist/log.js.map +1 -1
- package/dist/mod/examples/ticker/service.js +2 -3
- package/dist/mod/examples/ticker/service.js.map +1 -1
- package/dist/mod/index.d.ts +1 -1
- package/dist/mod/index.d.ts.map +1 -1
- package/dist/mod/index.js +1 -1
- package/dist/mod/index.js.map +1 -1
- package/dist/mod/loader.d.ts +1 -0
- package/dist/mod/loader.d.ts.map +1 -1
- package/dist/mod/loader.js +27 -1
- package/dist/mod/loader.js.map +1 -1
- package/dist/mods/clients.d.ts +2 -0
- package/dist/mods/clients.d.ts.map +1 -0
- package/dist/mods/clients.js +3 -0
- package/dist/mods/clients.js.map +1 -0
- package/dist/mods/llm/index.js +1 -1
- package/dist/mods/llm/index.js.map +1 -1
- package/dist/mods/mcp/server.d.ts +0 -1
- package/dist/mods/mcp/server.js +0 -1
- package/dist/mods/mcp/service.d.ts +0 -1
- package/dist/mods/mcp/service.js +0 -1
- package/dist/mods/mcp/types.d.ts +0 -1
- package/dist/mods/mcp/types.js +0 -1
- package/dist/mods/servers.d.ts +4 -0
- package/dist/mods/servers.d.ts.map +1 -0
- package/dist/mods/servers.js +5 -0
- package/dist/mods/servers.js.map +1 -0
- package/dist/mods/treenity/builtins.d.ts +2 -0
- package/dist/mods/treenity/builtins.d.ts.map +1 -0
- package/dist/mods/treenity/builtins.js +18 -0
- package/dist/mods/treenity/builtins.js.map +1 -0
- package/dist/mods/treenity/logs.d.ts +18 -0
- package/dist/mods/treenity/logs.d.ts.map +1 -0
- package/dist/mods/treenity/logs.js +17 -0
- package/dist/mods/treenity/logs.js.map +1 -0
- package/dist/mods/treenity/seed.js +29 -27
- package/dist/mods/treenity/seed.js.map +1 -1
- package/dist/mods/treenity/server.d.ts +2 -0
- package/dist/mods/treenity/server.d.ts.map +1 -1
- package/dist/mods/treenity/server.js +2 -0
- package/dist/mods/treenity/server.js.map +1 -1
- package/dist/mods/uix/client.js +4 -4
- package/dist/mods/uix/client.js.map +1 -1
- package/dist/mods/uix/compile.d.ts.map +1 -1
- package/dist/mods/uix/compile.js +4 -2
- package/dist/mods/uix/compile.js.map +1 -1
- package/dist/schema/_test-fixture.d.ts +11 -0
- package/dist/schema/_test-fixture.d.ts.map +1 -0
- package/dist/schema/_test-fixture.js +8 -0
- package/dist/schema/_test-fixture.js.map +1 -0
- package/dist/schema/types.d.ts +1 -0
- package/dist/schema/types.d.ts.map +1 -1
- package/dist/server/actions.js +1 -1
- package/dist/server/auth.d.ts.map +1 -1
- package/dist/server/auth.js +4 -3
- package/dist/server/auth.js.map +1 -1
- package/dist/server/client.d.ts +10 -3
- package/dist/server/client.d.ts.map +1 -1
- package/dist/server/client.js +1 -1
- package/dist/server/client.js.map +1 -1
- package/dist/server/doc-index.d.ts.map +1 -1
- package/dist/server/doc-index.js +13 -12
- package/dist/server/doc-index.js.map +1 -1
- package/dist/server/factory.d.ts +6 -0
- package/dist/server/factory.d.ts.map +1 -1
- package/dist/server/factory.js +44 -25
- package/dist/server/factory.js.map +1 -1
- package/dist/server/main.d.ts +0 -4
- package/dist/server/main.d.ts.map +1 -1
- package/dist/server/main.js +4 -30
- package/dist/server/main.js.map +1 -1
- package/dist/server/mcp.d.ts +0 -1
- package/dist/server/mcp.js +0 -1
- package/dist/server/migrate.js +3 -3
- package/dist/server/migrate.js.map +1 -1
- package/dist/server/mods-mount.d.ts +0 -1
- package/dist/server/mods-mount.d.ts.map +1 -1
- package/dist/server/mods-mount.js +0 -1
- package/dist/server/mods-mount.js.map +1 -1
- package/dist/server/mount-adapters.js +9 -4
- package/dist/server/mount-adapters.js.map +1 -1
- package/dist/server/prefab.d.ts +2 -2
- package/dist/server/prefab.d.ts.map +1 -1
- package/dist/server/prefab.js +4 -2
- package/dist/server/prefab.js.map +1 -1
- package/dist/server/refs.d.ts +3 -0
- package/dist/server/refs.d.ts.map +1 -0
- package/dist/server/refs.js +59 -0
- package/dist/server/refs.js.map +1 -0
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +13 -3
- package/dist/server/server.js.map +1 -1
- package/dist/server/sub.js.map +1 -1
- package/dist/server/trpc.d.ts +7 -0
- package/dist/server/trpc.d.ts.map +1 -1
- package/dist/server/trpc.js +6 -2
- package/dist/server/trpc.js.map +1 -1
- package/dist/tree/fs.d.ts.map +1 -1
- package/dist/tree/fs.js +20 -18
- package/dist/tree/fs.js.map +1 -1
- package/dist/tree/index.d.ts.map +1 -1
- package/dist/tree/index.js +2 -1
- package/dist/tree/index.js.map +1 -1
- package/dist/tree-chain.d.ts +2 -1
- package/dist/tree-chain.d.ts.map +1 -1
- package/dist/tree-chain.js +5 -3
- package/dist/tree-chain.js.map +1 -1
- package/dist/uri.d.ts.map +1 -1
- package/dist/uri.js +8 -3
- package/dist/uri.js.map +1 -1
- package/package.json +48 -6
- package/src/chain.ts +7 -5
- package/src/client/trpc.ts +1 -0
- package/src/comp/index.test.ts +45 -9
- package/src/comp/index.ts +19 -8
- package/src/comp/needs.ts +3 -3
- package/src/core/component.ts +16 -14
- package/src/core/context.ts +4 -4
- package/src/core/index.test.ts +22 -1
- package/src/core/path.ts +6 -3
- package/src/core/registry.ts +4 -7
- package/src/core.ts +1 -1
- package/src/index.ts +1 -0
- package/src/log.ts +172 -1
- package/src/mod/docs/07-realtime.md +19 -11
- package/src/mod/docs/08-services.md +10 -8
- package/src/mod/docs/10-acl.md +1 -1
- package/src/mod/docs/12-conventions.md +43 -0
- package/src/mod/docs/13-example.md +36 -26
- package/src/mod/docs/14-mod-format.md +81 -8
- package/src/mod/examples/ticker/service.ts +2 -3
- package/src/mod/index.ts +1 -1
- package/src/mod/loader.test.ts +53 -1
- package/src/mod/loader.ts +34 -1
- package/src/mods/clients.ts +2 -0
- package/src/mods/llm/index.ts +1 -1
- package/src/mods/servers.ts +4 -0
- package/src/mods/treenity/builtins.ts +21 -0
- package/src/mods/treenity/logs.ts +26 -0
- package/src/mods/treenity/seed.ts +30 -28
- package/src/mods/treenity/server.ts +2 -0
- package/src/mods/uix/client.ts +4 -4
- package/src/mods/uix/compile.ts +4 -2
- package/src/schema/_test-fixture.ts +12 -0
- package/src/schema/generated/ai.agent.json +133 -0
- package/src/schema/generated/ai.approval.json +105 -0
- package/src/schema/generated/ai.approvals.json +24 -0
- package/src/schema/generated/ai.assignment.json +28 -0
- package/src/schema/generated/ai.plan.json +84 -0
- package/src/schema/generated/ai.policy.json +105 -0
- package/src/schema/generated/ai.pool.json +37 -0
- package/src/schema/generated/ai.thread.json +64 -0
- package/src/schema/generated/board.kanban.json +7 -0
- package/src/schema/generated/canary.item.json +40 -0
- package/src/schema/generated/claude-search.json +20 -0
- package/src/schema/generated/craft.product.json +47 -0
- package/src/schema/generated/craft.shop.json +94 -0
- package/src/schema/generated/craft.subscription.json +27 -0
- package/src/schema/generated/examples.demo.sensor.reading.json +25 -0
- package/src/schema/generated/flow.node.action.json +61 -0
- package/src/schema/generated/flow.node.code.json +43 -0
- package/src/schema/generated/flow.node.condition.json +37 -0
- package/src/schema/generated/flow.node.end.json +35 -0
- package/src/schema/generated/flow.node.http.json +65 -0
- package/src/schema/generated/flow.node.llm.json +67 -0
- package/src/schema/generated/flow.node.loop.json +49 -0
- package/src/schema/generated/flow.node.start.json +39 -0
- package/src/schema/generated/flow.scenario.json +118 -0
- package/src/schema/generated/grove.attempt.json +199 -0
- package/src/schema/generated/grove.path.json +93 -0
- package/src/schema/generated/grove.review.json +27 -0
- package/src/schema/generated/grove.task.json +164 -0
- package/src/schema/generated/intel.scenario.json +58 -0
- package/src/schema/generated/intel.signal.json +113 -0
- package/src/schema/generated/intel.world.json +15 -0
- package/src/schema/generated/landing.block.json +201 -0
- package/src/schema/generated/landing.page.json +84 -0
- package/src/schema/generated/metatron.config.json +119 -0
- package/src/schema/generated/metatron.permission.json +36 -0
- package/src/schema/generated/metatron.skill.json +36 -0
- package/src/schema/generated/metatron.task.json +114 -0
- package/src/schema/generated/metatron.template.json +26 -0
- package/src/schema/generated/metatron.workspace.json +60 -0
- package/src/schema/generated/order.status.json +21 -0
- package/src/schema/generated/polyhope.backtest.json +161 -0
- package/src/schema/generated/polyhope.feed.json +33 -0
- package/src/schema/generated/polyhope.run.json +94 -0
- package/src/schema/generated/polyhope.strategy.json +152 -0
- package/src/schema/generated/polymax.activity.json +65 -0
- package/src/schema/generated/polymax.aggr-feed.json +28 -0
- package/src/schema/generated/polymax.aggr.json +20 -0
- package/src/schema/generated/polymax.alert.json +56 -0
- package/src/schema/generated/polymax.bot-config.json +14 -0
- package/src/schema/generated/polymax.bot-status.json +35 -0
- package/src/schema/generated/polymax.classification.json +55 -0
- package/src/schema/generated/polymax.deposits.json +45 -0
- package/src/schema/generated/polymax.holding.json +55 -0
- package/src/schema/generated/polymax.identity.json +71 -0
- package/src/schema/generated/polymax.lb-entry.json +75 -0
- package/src/schema/generated/polymax.leaderboard.json +37 -0
- package/src/schema/generated/polymax.market-ref.json +25 -0
- package/src/schema/generated/polymax.performance.json +65 -0
- package/src/schema/generated/polymax.profile.json +16 -0
- package/src/schema/generated/polymax.scan-result.json +50 -0
- package/src/schema/generated/polymax.status.json +40 -0
- package/src/schema/generated/polymax.tags.json +53 -0
- package/src/schema/generated/polymax.trader.json +16 -0
- package/src/schema/generated/polymax.wallet-market.json +50 -0
- package/src/schema/generated/polymax.wallet-pnl.json +35 -0
- package/src/schema/generated/pult.concept.json +53 -0
- package/src/schema/generated/pult.config.json +227 -0
- package/src/schema/generated/pult.connector.json +72 -0
- package/src/schema/generated/pult.market.json +113 -0
- package/src/schema/generated/pult.order.json +113 -0
- package/src/schema/generated/pult.rete.json +68 -0
- package/src/schema/generated/pult.sensor.json +74 -0
- package/src/schema/generated/pult.signal.json +54 -0
- package/src/schema/generated/pult.synapse.json +93 -0
- package/src/schema/generated/pult.trade.json +93 -0
- package/src/schema/generated/resim.config.json +34 -0
- package/src/schema/generated/resim.function.json +62 -0
- package/src/schema/generated/resim.goal.json +22 -0
- package/src/schema/generated/resim.resource.json +40 -0
- package/src/schema/generated/resim.state.json +48 -0
- package/src/schema/generated/resim.world.json +40 -0
- package/src/schema/generated/saveme.action.save.json +29 -0
- package/src/schema/generated/saveme.message.json +36 -0
- package/src/schema/generated/saveme.router.json +31 -0
- package/src/schema/generated/t.coolify.json +50 -0
- package/src/schema/generated/t.event.json +46 -0
- package/src/schema/generated/t.logs.json +155 -0
- package/src/schema/generated/t.note.json +31 -0
- package/src/schema/generated/t.person.json +36 -0
- package/src/schema/generated/t.tenant.json +57 -0
- package/src/schema/generated/t.tenant.status.json +42 -0
- package/src/schema/generated/tagger.config.json +115 -0
- package/src/schema/generated/tagger.result.json +57 -0
- package/src/schema/generated/tagger.tree.json +36 -0
- package/src/schema/generated/ui.table.json +46 -0
- package/src/schema/types.ts +1 -0
- package/src/server/actions.test.ts +1 -1
- package/src/server/actions.ts +1 -1
- package/src/server/api.test.ts +9 -0
- package/src/server/auth.ts +4 -3
- package/src/server/client.ts +1 -1
- package/src/server/coverage.test.ts +1 -1
- package/src/server/doc-index.ts +13 -12
- package/src/server/e2e.test.ts +4 -3
- package/src/server/factory.ts +46 -24
- package/src/server/main.ts +4 -36
- package/src/server/migrate.ts +4 -4
- package/src/server/mods-mount.ts +0 -2
- package/src/server/mount-adapters.ts +9 -4
- package/src/server/mount.test.ts +73 -5
- package/src/server/prefab.ts +3 -2
- package/src/server/refs.test.ts +82 -0
- package/src/server/refs.ts +64 -0
- package/src/server/server.ts +14 -3
- package/src/server/sub.ts +2 -2
- package/src/server/trpc.ts +9 -3
- package/src/tree/fs.ts +21 -15
- package/src/tree/index.test.ts +26 -0
- package/src/tree/index.ts +2 -1
- package/src/tree-chain.test.ts +37 -44
- package/src/tree-chain.ts +11 -5
- package/src/uri.test.ts +32 -5
- package/src/uri.ts +4 -2
- package/dist/mods/mcp/server.d.ts.map +0 -1
- package/dist/mods/mcp/server.js.map +0 -1
- package/dist/mods/mcp/service.d.ts.map +0 -1
- package/dist/mods/mcp/service.js.map +0 -1
- package/dist/mods/mcp/types.d.ts.map +0 -1
- package/dist/mods/mcp/types.js.map +0 -1
- package/dist/schema/test-opaque.d.ts +0 -3
- package/dist/schema/test-opaque.d.ts.map +0 -1
- package/dist/schema/test-opaque.js +0 -43
- package/dist/schema/test-opaque.js.map +0 -1
- package/dist/server/mcp.d.ts.map +0 -1
- package/dist/server/mcp.js.map +0 -1
- package/src/mods/mcp/CLAUDE.md +0 -6
- package/src/mods/mcp/server.ts +0 -2
- package/src/mods/mcp/service.ts +0 -19
- package/src/mods/mcp/types.ts +0 -7
- package/src/schema/test-opaque.ts +0 -42
- package/src/server/mcp.ts +0 -326
package/src/core/component.ts
CHANGED
|
@@ -13,25 +13,28 @@ export type ComponentData<T = Record<string, unknown>> = T & {
|
|
|
13
13
|
$acl?: GroupPerm[];
|
|
14
14
|
};
|
|
15
15
|
|
|
16
|
+
export type RefEntry = { t: string; f?: string; d?: ComponentData };
|
|
17
|
+
|
|
16
18
|
export type NodeData<T = Record<string, unknown>> = ComponentData<T> & {
|
|
17
19
|
$path: string;
|
|
18
20
|
$owner?: string;
|
|
19
21
|
$rev?: number;
|
|
22
|
+
$refs?: RefEntry[];
|
|
20
23
|
};
|
|
21
24
|
|
|
22
25
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
|
|
23
26
|
export type Class<T = unknown> = new (...args: any[]) => T;
|
|
24
27
|
|
|
25
|
-
// Accept string
|
|
26
|
-
export type TypeId<T = unknown> = string |
|
|
28
|
+
// Accept string or registered class (registerType stamps $type on constructor)
|
|
29
|
+
export type TypeId<T = unknown> = string | Class<T>;
|
|
27
30
|
|
|
28
31
|
// ── Type normalization ──
|
|
29
32
|
// Dot-less types belong to treenity namespace: 'dir' → 't.dir', 'ref' → 't.ref'
|
|
30
33
|
// Types with dots are already namespaced and returned as-is
|
|
31
34
|
export function normalizeType(type: TypeId): string {
|
|
32
35
|
if (typeof type === 'string') return type.includes('.') ? type : `t.${type}`;
|
|
33
|
-
if (
|
|
34
|
-
throw new Error('TypeId:
|
|
36
|
+
if (typeof (type as any).$type === 'string') return normalizeType((type as any).$type);
|
|
37
|
+
throw new Error('TypeId: class not registered (missing $type)');
|
|
35
38
|
}
|
|
36
39
|
|
|
37
40
|
// ── Utils ──
|
|
@@ -40,17 +43,12 @@ export function isComponent(value: unknown): value is ComponentData {
|
|
|
40
43
|
return typeof value === 'object' && value !== null && '$type' in value;
|
|
41
44
|
}
|
|
42
45
|
|
|
43
|
-
export
|
|
44
|
-
const v = key === '' ? node : node[key];
|
|
45
|
-
return isComponent(v) ? v : undefined;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
export const AnyType = { $type: 'any' };
|
|
46
|
+
export const AnyType = 't.any';
|
|
49
47
|
|
|
50
48
|
export function isOfType<T>(value: unknown, type: TypeId): value is ComponentData<T> {
|
|
51
49
|
if (!isComponent(value)) return false;
|
|
52
50
|
const t = normalizeType(type);
|
|
53
|
-
return t ===
|
|
51
|
+
return t === AnyType || t === normalizeType(value.$type);
|
|
54
52
|
}
|
|
55
53
|
|
|
56
54
|
// ── Ref ──
|
|
@@ -72,13 +70,17 @@ export function assertNonSystemName(name: string) {
|
|
|
72
70
|
if (name.startsWith('$')) throw new Error(`Component name cannot start with $: ${name}`);
|
|
73
71
|
}
|
|
74
72
|
|
|
73
|
+
export function createNode<T, C = Record<string, ComponentData<any>>>(
|
|
74
|
+
path: string, type: Class<T>, data?: Partial<T>, components?: C): NodeData<T & C>;
|
|
75
75
|
export function createNode<T = any, C = Record<string, ComponentData<any>>>(
|
|
76
|
+
path: string, type: string, data?: T, components?: C): NodeData<T & C>;
|
|
77
|
+
export function createNode(
|
|
76
78
|
path: string,
|
|
77
79
|
type: TypeId,
|
|
78
|
-
data?:
|
|
79
|
-
components?:
|
|
80
|
+
data?: any,
|
|
81
|
+
components?: any): NodeData {
|
|
80
82
|
|
|
81
|
-
const node: NodeData
|
|
83
|
+
const node: NodeData = { $path: path, $type: normalizeType(type) } as NodeData;
|
|
82
84
|
if (components) Object.keys(components).forEach(assertNonSystemName);
|
|
83
85
|
if (data) Object.keys(data).forEach(assertNonSystemName);
|
|
84
86
|
|
package/src/core/context.ts
CHANGED
|
@@ -4,11 +4,11 @@ export type Handler = (...args: any[]) => any;
|
|
|
4
4
|
|
|
5
5
|
// Typed context handlers — augmented by layers via declaration merging
|
|
6
6
|
// eslint-disable-next-line @typescript-eslint/no-empty-interface
|
|
7
|
-
export interface ContextHandlers {
|
|
7
|
+
export interface ContextHandlers<T = any> {
|
|
8
8
|
}
|
|
9
9
|
|
|
10
|
-
export type ContextHandler<C extends string> = C extends keyof ContextHandlers
|
|
11
|
-
? ContextHandlers[C]
|
|
10
|
+
export type ContextHandler<C extends string, T = any> = C extends keyof ContextHandlers<T>
|
|
11
|
+
? ContextHandlers<T>[C]
|
|
12
12
|
: C extends `${infer Base}:${string}`
|
|
13
|
-
? ContextHandler<Base>
|
|
13
|
+
? ContextHandler<Base, T>
|
|
14
14
|
: Handler;
|
package/src/core/index.test.ts
CHANGED
|
@@ -15,9 +15,21 @@ import {
|
|
|
15
15
|
resolve,
|
|
16
16
|
unregister,
|
|
17
17
|
} from './index';
|
|
18
|
+
import { registerBuiltins } from '#mods/treenity/builtins';
|
|
19
|
+
|
|
20
|
+
const testTypes = ['test.doc', 'test.item', 'test.session', 'test.task'];
|
|
21
|
+
|
|
22
|
+
export function registerTestTypes() {
|
|
23
|
+
for (const t of testTypes)
|
|
24
|
+
register(t, 'schema', () => ({ $id: t, type: 'object' as const, title: t, properties: {} }));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
registerTestTypes();
|
|
18
28
|
|
|
19
29
|
export function clearRegistry(): void {
|
|
20
30
|
mapRegistry((t, c) => unregister(t, c));
|
|
31
|
+
registerBuiltins();
|
|
32
|
+
registerTestTypes();
|
|
21
33
|
}
|
|
22
34
|
|
|
23
35
|
describe('Node', () => {
|
|
@@ -81,7 +93,7 @@ describe('Component access', () => {
|
|
|
81
93
|
|
|
82
94
|
it('isComponent type guard', () => {
|
|
83
95
|
assert.ok(isComponent(node['budget']));
|
|
84
|
-
assert.ok(!isComponent(node['missing']));
|
|
96
|
+
assert.ok(!isComponent((node as any)['missing']));
|
|
85
97
|
assert.ok(!isComponent('string'));
|
|
86
98
|
assert.ok(!isComponent(null));
|
|
87
99
|
});
|
|
@@ -141,6 +153,15 @@ describe('Path utils', () => {
|
|
|
141
153
|
assert.equal(isChildPath('/tasks', '/tasks/123/sub', false), true);
|
|
142
154
|
assert.equal(isChildPath('/', '/tasks/123', false), true);
|
|
143
155
|
});
|
|
156
|
+
|
|
157
|
+
// Regression: /board must not match /boards (prefix overlap without separator)
|
|
158
|
+
it('isChildPath rejects prefix overlap without separator', () => {
|
|
159
|
+
assert.equal(isChildPath('/board', '/boards'), false);
|
|
160
|
+
assert.equal(isChildPath('/board', '/boards/test'), false);
|
|
161
|
+
assert.equal(isChildPath('/board', '/boards/test', false), false);
|
|
162
|
+
assert.equal(isChildPath('/board', '/board/real'), true);
|
|
163
|
+
assert.equal(isChildPath('/board', '/board/real/deep', false), true);
|
|
164
|
+
});
|
|
144
165
|
});
|
|
145
166
|
|
|
146
167
|
describe('Context', () => {
|
package/src/core/path.ts
CHANGED
|
@@ -17,10 +17,13 @@ export function join(parent: string, name: string): string {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function isChildPath(parent: string, candidate: string, directOnly = true): boolean {
|
|
20
|
-
if (
|
|
20
|
+
if (candidate === parent) return false;
|
|
21
|
+
// Must match parent + '/' to avoid /board matching /boards
|
|
22
|
+
const prefix = parent === '/' ? '/' : parent + '/';
|
|
23
|
+
if (!candidate.startsWith(prefix)) return false;
|
|
21
24
|
if (directOnly) {
|
|
22
|
-
const rest =
|
|
23
|
-
return !rest.includes('/');
|
|
25
|
+
const rest = candidate.slice(prefix.length);
|
|
26
|
+
return rest.length > 0 && !rest.includes('/');
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
return true;
|
package/src/core/registry.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ComponentData, normalizeType, type TypeId } from './component';
|
|
1
|
+
import { type Class, ComponentData, normalizeType, type TypeId } from './component';
|
|
2
2
|
import { ContextHandler, Handler } from './context';
|
|
3
3
|
|
|
4
4
|
const registry = new Map<string, Handler>();
|
|
@@ -21,12 +21,9 @@ function key(type: string, context: string): string {
|
|
|
21
21
|
return `${type}@${context}`;
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export function register<C extends string>(
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
handler: ContextHandler<C>,
|
|
28
|
-
meta?: Record<string, unknown>,
|
|
29
|
-
): void {
|
|
24
|
+
export function register<C extends string>(type: string, context: C, handler: ContextHandler<C>, meta?: Record<string, unknown>): void;
|
|
25
|
+
export function register<T, C extends string>(type: Class<T>, context: C, handler: ContextHandler<C, T>, meta?: Record<string, unknown>): void;
|
|
26
|
+
export function register(type: TypeId, context: string, handler: Handler, meta?: Record<string, unknown>): void {
|
|
30
27
|
const k = key(normalizeType(type), context);
|
|
31
28
|
// Sealed: no overrides. In dev (HMR) modules re-execute, so we allow silent replace.
|
|
32
29
|
// In production, duplicate register = bug, caught by tests.
|
package/src/core.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export * from './core/index
|
|
1
|
+
export * from './core/index';
|
package/src/index.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './core/index';
|
package/src/log.ts
CHANGED
|
@@ -1,3 +1,150 @@
|
|
|
1
|
+
// Unified logging: tree-persisted nodes + ring buffer fallback + debug filter + console intercept
|
|
2
|
+
|
|
3
|
+
import dayjs from 'dayjs'
|
|
4
|
+
|
|
5
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error'
|
|
6
|
+
|
|
7
|
+
export interface LogEntry {
|
|
8
|
+
t: number
|
|
9
|
+
level: LogLevel
|
|
10
|
+
msg: string
|
|
11
|
+
code?: string
|
|
12
|
+
sub?: string
|
|
13
|
+
userId?: string
|
|
14
|
+
method?: string
|
|
15
|
+
path?: string
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// ── Execution context provider — set by comp/index.ts to avoid circular imports ──
|
|
19
|
+
|
|
20
|
+
type CtxProvider = () => Record<string, unknown> | null
|
|
21
|
+
let _getCtx: CtxProvider = () => null
|
|
22
|
+
|
|
23
|
+
export function setCtxProvider(fn: CtxProvider) { _getCtx = fn }
|
|
24
|
+
|
|
25
|
+
// ── Log listeners ──
|
|
26
|
+
|
|
27
|
+
type OnLog = (entry: LogEntry) => void
|
|
28
|
+
const listeners: OnLog[] = []
|
|
29
|
+
|
|
30
|
+
export function addOnLog(fn: OnLog) { listeners.push(fn) }
|
|
31
|
+
|
|
32
|
+
// ── Timestamp ID: YYMMDD-HHmmss-mmm-NNN ──
|
|
33
|
+
|
|
34
|
+
let lastMs = 0
|
|
35
|
+
let seq = 0
|
|
36
|
+
|
|
37
|
+
export function makeLogPath(): string {
|
|
38
|
+
const now = Date.now()
|
|
39
|
+
if (now === lastMs) {
|
|
40
|
+
seq++
|
|
41
|
+
} else {
|
|
42
|
+
lastMs = now
|
|
43
|
+
seq = 0
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const stamp = dayjs(now).format('YYMMDD-HHmmss-SSS')
|
|
47
|
+
const sq = String(seq).padStart(3, '0')
|
|
48
|
+
|
|
49
|
+
return `/sys/logs/${stamp}-${sq}`
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function notify(entry: LogEntry) {
|
|
53
|
+
for (const fn of listeners) fn(entry)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Ring buffer (fallback before tree init) ──
|
|
57
|
+
|
|
58
|
+
const MAX = 2000
|
|
59
|
+
const buffer: LogEntry[] = []
|
|
60
|
+
let cursor = 0
|
|
61
|
+
let total = 0
|
|
62
|
+
|
|
63
|
+
function push(level: LogLevel, args: unknown[]) {
|
|
64
|
+
// Extract [tag] → sub
|
|
65
|
+
let sub: string | undefined
|
|
66
|
+
if (typeof args[0] === 'string') {
|
|
67
|
+
const m = args[0].match(/^\[([^\]]+)\]$/)
|
|
68
|
+
if (m) {
|
|
69
|
+
sub = m[1]
|
|
70
|
+
args = args.slice(1)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Extract UPPER_SNAKE code
|
|
75
|
+
let code: string | undefined
|
|
76
|
+
if (args.length > 1 && typeof args[0] === 'string' && /^[A-Z][A-Z0-9_]+$/.test(args[0])) {
|
|
77
|
+
code = args[0]
|
|
78
|
+
args = args.slice(1)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const msg = args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ')
|
|
82
|
+
const ctx = _getCtx()
|
|
83
|
+
|
|
84
|
+
const entry: LogEntry = {
|
|
85
|
+
t: Date.now(),
|
|
86
|
+
level,
|
|
87
|
+
msg,
|
|
88
|
+
code,
|
|
89
|
+
sub,
|
|
90
|
+
userId: ctx?.userId as string | undefined,
|
|
91
|
+
method: ctx?.method as string | undefined,
|
|
92
|
+
path: ctx?.path as string | undefined,
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (listeners.length) {
|
|
96
|
+
notify(entry)
|
|
97
|
+
} else {
|
|
98
|
+
if (total < MAX) {
|
|
99
|
+
buffer.push(entry)
|
|
100
|
+
} else {
|
|
101
|
+
buffer[cursor] = entry
|
|
102
|
+
}
|
|
103
|
+
cursor = (cursor + 1) % MAX
|
|
104
|
+
total++
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Get ordered log entries from ring buffer (oldest first) */
|
|
109
|
+
function getOrdered(): LogEntry[] {
|
|
110
|
+
if (total <= MAX) return buffer.slice()
|
|
111
|
+
return [...buffer.slice(cursor), ...buffer.slice(0, cursor)]
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Query (ring buffer fallback — when tree available, use sift via getChildren) ──
|
|
115
|
+
|
|
116
|
+
export interface LogQuery {
|
|
117
|
+
grep?: string
|
|
118
|
+
level?: LogLevel | LogLevel[]
|
|
119
|
+
head?: number
|
|
120
|
+
tail?: number
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
export function queryLogs(opts: LogQuery = {}): LogEntry[] {
|
|
124
|
+
let entries = getOrdered()
|
|
125
|
+
|
|
126
|
+
if (opts.level) {
|
|
127
|
+
const levels = Array.isArray(opts.level) ? opts.level : [opts.level]
|
|
128
|
+
entries = entries.filter(e => levels.includes(e.level))
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
if (opts.grep) {
|
|
132
|
+
const re = new RegExp(opts.grep, 'i')
|
|
133
|
+
entries = entries.filter(e => re.test(e.msg))
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
if (opts.tail) entries = entries.slice(-opts.tail)
|
|
137
|
+
if (opts.head) entries = entries.slice(0, opts.head)
|
|
138
|
+
|
|
139
|
+
return entries
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function logStats() {
|
|
143
|
+
return { buffered: Math.min(total, MAX), total, max: MAX }
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ── Debug filter ──
|
|
147
|
+
|
|
1
148
|
const enabled = new Set<string>()
|
|
2
149
|
let all = false
|
|
3
150
|
|
|
@@ -15,7 +162,7 @@ if (typeof process !== 'undefined' && process.env?.DEBUG) {
|
|
|
15
162
|
setDebug(process.env.DEBUG)
|
|
16
163
|
}
|
|
17
164
|
|
|
18
|
-
;(globalThis as
|
|
165
|
+
;(globalThis as Record<string, unknown>).setDebug = setDebug
|
|
19
166
|
|
|
20
167
|
export function createLogger(name: string) {
|
|
21
168
|
const tag = `[${name}]`
|
|
@@ -26,3 +173,27 @@ export function createLogger(name: string) {
|
|
|
26
173
|
error(...args: unknown[]) { console.error(tag, ...args) },
|
|
27
174
|
}
|
|
28
175
|
}
|
|
176
|
+
|
|
177
|
+
// ── Console intercept — call once at startup ──
|
|
178
|
+
|
|
179
|
+
let intercepted = false
|
|
180
|
+
|
|
181
|
+
export function interceptConsole() {
|
|
182
|
+
if (intercepted) return
|
|
183
|
+
intercepted = true
|
|
184
|
+
|
|
185
|
+
for (const level of ['debug', 'info', 'warn', 'error'] as const) {
|
|
186
|
+
const orig = console[level]
|
|
187
|
+
console[level] = (...args: unknown[]) => {
|
|
188
|
+
push(level, args)
|
|
189
|
+
orig.apply(console, args)
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
// console.log → info level
|
|
194
|
+
const origLog = console.log
|
|
195
|
+
console.log = (...args: unknown[]) => {
|
|
196
|
+
push('info', args)
|
|
197
|
+
origLog.apply(console, args)
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -66,19 +66,23 @@ function MyRenderer({ value, onChange }) {
|
|
|
66
66
|
## react:list — самодостаточные list-item компоненты
|
|
67
67
|
|
|
68
68
|
```tsx
|
|
69
|
-
|
|
69
|
+
import type { View } from '@treenity/react/context';
|
|
70
|
+
|
|
71
|
+
// View для default — value: ComponentData (базовый тип, есть $type)
|
|
72
|
+
const DefaultListItem: View<ComponentData> = ({ value, ctx }) => {
|
|
73
|
+
const node = ctx!.node;
|
|
70
74
|
return (
|
|
71
|
-
<div className="child-card" onClick={() => navigate(
|
|
72
|
-
<span className="child-icon">{typeIcon(
|
|
75
|
+
<div className="child-card" onClick={() => navigate(node.$path)}>
|
|
76
|
+
<span className="child-icon">{typeIcon(node.$type)}</span>
|
|
73
77
|
<div className="child-info">
|
|
74
|
-
<span className="child-name">{pathName(
|
|
75
|
-
<span className="child-type">{
|
|
78
|
+
<span className="child-name">{pathName(node.$path)}</span>
|
|
79
|
+
<span className="child-type">{node.$type}</span>
|
|
76
80
|
</div>
|
|
77
81
|
<span className="child-chevron">›</span>
|
|
78
82
|
</div>
|
|
79
83
|
);
|
|
80
|
-
}
|
|
81
|
-
register('default', 'react:list', DefaultListItem
|
|
84
|
+
};
|
|
85
|
+
register('default', 'react:list', DefaultListItem);
|
|
82
86
|
```
|
|
83
87
|
|
|
84
88
|
Fallback: `react:list` → `default@react:list` → strip `:list` → `react`.
|
|
@@ -88,11 +92,15 @@ Fallback: `react:list` → `default@react:list` → strip `:list` → `react`.
|
|
|
88
92
|
Когда `node.$type` совпадает с `$type` компонента, нода **сама является** компонентом. Поля лежат плоско:
|
|
89
93
|
|
|
90
94
|
```ts
|
|
91
|
-
|
|
92
|
-
|
|
95
|
+
import { createNode } from '#core';
|
|
96
|
+
|
|
97
|
+
// Правильно — createNode создаёт типизированную ноду
|
|
98
|
+
await store.set(createNode('/bot', 'brahman.bot', { token: '...', alias: '@bot' }));
|
|
93
99
|
|
|
94
|
-
// Неправильно
|
|
95
|
-
await store.set(
|
|
100
|
+
// Неправильно — вложенный компонент с тем же $type что и нода
|
|
101
|
+
await store.set(createNode('/bot', 'brahman.bot', {
|
|
102
|
+
config: { $type: 'brahman.bot', token: '...' }, // WRONG: дублирует $type
|
|
103
|
+
}));
|
|
96
104
|
```
|
|
97
105
|
|
|
98
106
|
## Views — read-only
|
|
@@ -3,16 +3,16 @@
|
|
|
3
3
|
Фоновые процессы: боты, сенсоры, воркеры. `register(type, "service", handler)` → возвращает `{ stop() }`.
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
|
-
import { register, type NodeData } from '#core';
|
|
6
|
+
import { register, createNode, type NodeData } from '#core';
|
|
7
7
|
import { type ServiceHandle, type ServiceCtx } from '#contexts/service';
|
|
8
8
|
|
|
9
9
|
register('my-worker', 'service', async (node: NodeData, ctx: ServiceCtx) => {
|
|
10
10
|
const timer = setInterval(async () => {
|
|
11
|
-
await ctx.store.set(
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
ts: Date.now(),
|
|
15
|
-
|
|
11
|
+
await ctx.store.set(createNode(
|
|
12
|
+
`${node.$path}/${Date.now()}`,
|
|
13
|
+
'tick',
|
|
14
|
+
{ ts: Date.now() },
|
|
15
|
+
));
|
|
16
16
|
}, 5000);
|
|
17
17
|
|
|
18
18
|
return {
|
|
@@ -26,8 +26,10 @@ register('my-worker', 'service', async (node: NodeData, ctx: ServiceCtx) => {
|
|
|
26
26
|
Создай ref-ноду в `/sys/autostart`:
|
|
27
27
|
|
|
28
28
|
```ts
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
import { createNode } from '#core';
|
|
30
|
+
|
|
31
|
+
await store.set(createNode('/my-worker', 'my-worker'));
|
|
32
|
+
await store.set(createNode('/sys/autostart/my-worker', 'ref', { $ref: '/my-worker' }));
|
|
31
33
|
```
|
|
32
34
|
|
|
33
35
|
При старте сервера `startServices(store)` обходит `/sys/autostart`, резолвит refs, вызывает service-хэндлеры.
|
package/src/mod/docs/10-acl.md
CHANGED
|
@@ -46,7 +46,7 @@ const validated = withValidation(store);
|
|
|
46
46
|
import { withVolatile } from '#server/volatile';
|
|
47
47
|
|
|
48
48
|
const store = withVolatile(backingStore);
|
|
49
|
-
await store.set(
|
|
49
|
+
await store.set(createNode('/temp', 'session', { $volatile: true }));
|
|
50
50
|
```
|
|
51
51
|
|
|
52
52
|
## Server Pipeline
|
|
@@ -40,6 +40,49 @@ t.* → treenity infrastructure: t.mount.fs, t.mount.mongo, t.llm
|
|
|
40
40
|
| `class` | `Class<T>` | Registered component class |
|
|
41
41
|
| `telegram` | `(node, tgCtx) => void` | Telegram-хэндлер |
|
|
42
42
|
|
|
43
|
+
## TypeScript — дисциплина типов
|
|
44
|
+
|
|
45
|
+
**`as any` ЗАПРЕЩЁН.** Не "только в emergency" — НИКОГДА. Если типы не сходятся, чини типы. `as any` прячет баги, ломает inference, расползается как рак.
|
|
46
|
+
|
|
47
|
+
Допустимые касты:
|
|
48
|
+
- `as const` — сужение литерала
|
|
49
|
+
- `as 'idle'` — сужение к известному union-варианту
|
|
50
|
+
- `as SensorReading` — сужение к конкретному типу (когда ты знаешь что это он)
|
|
51
|
+
|
|
52
|
+
**Создание нод — ВСЕГДА через `createNode()`:**
|
|
53
|
+
```ts
|
|
54
|
+
// Со строковым типом — T выводится из data:
|
|
55
|
+
await store.set(createNode('/path', 'my.type', { field: 'value' }));
|
|
56
|
+
|
|
57
|
+
// С Class<T> — data проверяется по полям класса (Partial<T>):
|
|
58
|
+
await store.set(createNode('/sensors/temp', SensorConfig, { interval: 10, source: 'api' }));
|
|
59
|
+
// ^^^^^^^^ autocomplete + type check
|
|
60
|
+
|
|
61
|
+
// С компонентами:
|
|
62
|
+
await store.set(createNode('/path', 'sensor', { interval: 5 }, {
|
|
63
|
+
config: { $type: 'sensor.config', source: 'api' },
|
|
64
|
+
}));
|
|
65
|
+
|
|
66
|
+
// ЗАПРЕЩЕНО — as NodeData убивает типизацию
|
|
67
|
+
await store.set({ $path: '/path', $type: 'my.type', field: 'value' } as NodeData);
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Views — типизированные через `View<T>`:**
|
|
71
|
+
```tsx
|
|
72
|
+
import type { View } from '@treenity/react/context';
|
|
73
|
+
|
|
74
|
+
// value — данные компонента. path — через ctx!.node.$path
|
|
75
|
+
const MyView: View<MyType> = ({ value, ctx }) => { ... };
|
|
76
|
+
|
|
77
|
+
// register принимает Class<T> — T пробрасывается автоматически
|
|
78
|
+
register(MyType, 'react', MyView);
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Запрещённые паттерны:**
|
|
82
|
+
- `value as NodeData & MyType` — value УЖЕ типизирован через View<T>
|
|
83
|
+
- `(node as any).field` — используй `getComp(node, Class)` для типобезопасного доступа
|
|
84
|
+
- `register('type', 'react', handler as any)` — используй `register(Class, 'react', handler)`
|
|
85
|
+
|
|
43
86
|
## Ошибки
|
|
44
87
|
|
|
45
88
|
```ts
|
|
@@ -4,8 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
```ts
|
|
6
6
|
// src/mods/sensor/types.ts
|
|
7
|
-
import { getCtx, registerComp } from '#comp';
|
|
8
|
-
import { join, type NodeData } from '#core';
|
|
7
|
+
import { getCtx, registerComp, getComp } from '#comp';
|
|
9
8
|
|
|
10
9
|
export class SensorConfig {
|
|
11
10
|
/** @title Интервал @description Секунды между замерами */
|
|
@@ -20,7 +19,10 @@ export class SensorConfig {
|
|
|
20
19
|
async history() {
|
|
21
20
|
const { node, store } = getCtx();
|
|
22
21
|
const { items } = await store.getChildren(node.$path, { limit: 100 });
|
|
23
|
-
return { items: items.map(n =>
|
|
22
|
+
return { items: items.map(n => {
|
|
23
|
+
const r = getComp(n, SensorReading);
|
|
24
|
+
return r ? { value: r.value, ts: r.ts } : null;
|
|
25
|
+
}).filter(Boolean) };
|
|
24
26
|
}
|
|
25
27
|
}
|
|
26
28
|
|
|
@@ -38,7 +40,7 @@ registerComp('sensor.reading', SensorReading);
|
|
|
38
40
|
|
|
39
41
|
```ts
|
|
40
42
|
// src/mods/sensor/service.ts
|
|
41
|
-
import { register, type NodeData } from '#core';
|
|
43
|
+
import { register, createNode, type NodeData } from '#core';
|
|
42
44
|
import { getComp } from '#comp';
|
|
43
45
|
import { type ServiceHandle, type ServiceCtx } from '#contexts/service';
|
|
44
46
|
import { SensorConfig } from './types';
|
|
@@ -48,12 +50,11 @@ register('sensor', 'service', async (node: NodeData, ctx: ServiceCtx) => {
|
|
|
48
50
|
const interval = (config?.interval ?? 5) * 1000;
|
|
49
51
|
|
|
50
52
|
const timer = setInterval(async () => {
|
|
51
|
-
await ctx.store.set(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
value: Math.random() * 100,
|
|
55
|
-
|
|
56
|
-
} as NodeData);
|
|
53
|
+
await ctx.store.set(createNode(
|
|
54
|
+
`${node.$path}/${Date.now()}`,
|
|
55
|
+
'sensor.reading',
|
|
56
|
+
{ value: Math.random() * 100, ts: Date.now() },
|
|
57
|
+
));
|
|
57
58
|
}, interval);
|
|
58
59
|
|
|
59
60
|
return { stop: async () => clearInterval(timer) } satisfies ServiceHandle;
|
|
@@ -64,34 +65,39 @@ register('sensor', 'service', async (node: NodeData, ctx: ServiceCtx) => {
|
|
|
64
65
|
|
|
65
66
|
```tsx
|
|
66
67
|
// src/mods/sensor/view.tsx
|
|
67
|
-
import { register
|
|
68
|
-
import {
|
|
69
|
-
import { useChildren } from '
|
|
68
|
+
import { register } from '#core';
|
|
69
|
+
import type { View } from '@treenity/react/context';
|
|
70
|
+
import { useChildren } from '@treenity/react/hooks';
|
|
71
|
+
import { SensorConfig, SensorReading } from './types';
|
|
70
72
|
|
|
71
|
-
|
|
73
|
+
// View<T> — типизированный компонент. value: T, ctx: ViewCtx
|
|
74
|
+
const ConfigView: View<SensorConfig> = ({ value }) => {
|
|
72
75
|
return (
|
|
73
76
|
<div>
|
|
74
|
-
<label>Interval: {
|
|
75
|
-
<label>Source: {
|
|
77
|
+
<label>Interval: {value.interval}s</label>
|
|
78
|
+
<label>Source: {value.source}</label>
|
|
76
79
|
</div>
|
|
77
80
|
);
|
|
78
|
-
}
|
|
81
|
+
};
|
|
79
82
|
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
const readings = useChildren(node.$path, { limit: 10, watchNew: true });
|
|
83
|
+
const SensorView: View<SensorConfig> = ({ value, ctx }) => {
|
|
84
|
+
const readings = useChildren(ctx!.node.$path, { limit: 10, watchNew: true });
|
|
83
85
|
|
|
84
86
|
return (
|
|
85
87
|
<div>
|
|
86
|
-
<h3>Sensor: {node.$path}</h3>
|
|
88
|
+
<h3>Sensor: {ctx!.node.$path}</h3>
|
|
87
89
|
{readings.map(r => (
|
|
88
90
|
<div key={r.$path}>
|
|
89
|
-
{(r as
|
|
91
|
+
{(r as SensorReading).value?.toFixed(1)} @ {new Date((r as SensorReading).ts).toLocaleTimeString()}
|
|
90
92
|
</div>
|
|
91
93
|
))}
|
|
92
94
|
</div>
|
|
93
95
|
);
|
|
94
|
-
}
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
// register принимает Class<T> — типы пробрасываются автоматически
|
|
99
|
+
register(SensorConfig, 'react', ConfigView);
|
|
100
|
+
register(SensorConfig, 'react:list', SensorView);
|
|
95
101
|
```
|
|
96
102
|
|
|
97
103
|
## 4. schemas.ts — JSON Schema
|
|
@@ -124,9 +130,13 @@ import './sensor/schemas';
|
|
|
124
130
|
## 6. Seed-данные
|
|
125
131
|
|
|
126
132
|
```ts
|
|
127
|
-
|
|
133
|
+
import { createNode } from '#core';
|
|
134
|
+
|
|
135
|
+
await store.set(createNode('/sensors/temp', 'sensor', {
|
|
128
136
|
config: { $type: 'sensor.config', interval: 10, source: 'internal' },
|
|
129
|
-
}
|
|
137
|
+
}));
|
|
130
138
|
|
|
131
|
-
await store.set(
|
|
139
|
+
await store.set(createNode('/sys/autostart/temp-sensor', 'ref', {
|
|
140
|
+
$ref: '/sensors/temp',
|
|
141
|
+
}));
|
|
132
142
|
```
|