@projectqai/proto 0.0.25 → 0.0.26

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/device.ts ADDED
@@ -0,0 +1,256 @@
1
+ import { createClient, type Client, ConnectError, Code } from "@connectrpc/connect";
2
+ import { createGrpcTransport } from "@connectrpc/connect-node";
3
+ import { create, clone } from "@bufbuild/protobuf";
4
+ import {
5
+ WorldService,
6
+ EntitySchema,
7
+ EntityFilterSchema,
8
+ ListEntitiesRequestSchema,
9
+ EntityChangeRequestSchema,
10
+ ConfigurableComponentSchema,
11
+ ConfigurableState,
12
+ EntityChange,
13
+ LifetimeSchema,
14
+ DeviceComponentSchema,
15
+ DeviceState,
16
+ ControllerSchema,
17
+ InteractivityComponentSchema,
18
+ type Entity,
19
+ } from "./dist/world_pb.js";
20
+ import { TimestampSchema } from "@bufbuild/protobuf/wkt";
21
+ import {
22
+ MetricComponentSchema,
23
+ MetricSchema,
24
+ MetricKind,
25
+ MetricUnit,
26
+ } from "./dist/metrics_pb.js";
27
+
28
+ export type WorldClient = Client<typeof WorldService>;
29
+
30
+ export { create } from "@bufbuild/protobuf";
31
+ export {
32
+ EntitySchema,
33
+ EntityFilterSchema,
34
+ ListEntitiesRequestSchema,
35
+ EntityChangeRequestSchema,
36
+ EntityChange,
37
+ type Entity,
38
+ } from "./dist/world_pb.js";
39
+
40
+ export function connect(serverURL?: string): WorldClient {
41
+ const base = serverURL ?? process.env.HYDRIS_SERVER ?? "http://localhost:50051";
42
+ const transport = createGrpcTransport({ baseUrl: base, httpVersion: "2" });
43
+ return createClient(WorldService, transport);
44
+ }
45
+
46
+ export function push(client: WorldClient, ...entities: Entity[]) {
47
+ return client.push(create(EntityChangeRequestSchema, { changes: entities }));
48
+ }
49
+
50
+ // Schema → typed config inference
51
+
52
+ type SchemaProperty = { readonly type: string; readonly default?: unknown; readonly [key: string]: unknown };
53
+ type SchemaProperties = Readonly<Record<string, SchemaProperty>>;
54
+
55
+ type InferProperty<P extends SchemaProperty> =
56
+ P extends { type: "string" } ? string :
57
+ P extends { type: "boolean" } ? boolean :
58
+ P extends { type: "number" | "integer" } ? number :
59
+ unknown;
60
+
61
+ export type InferConfig<S extends SchemaProperties> = {
62
+ [K in keyof S]?: InferProperty<S[K]>;
63
+ };
64
+
65
+ function extractConfig<S extends SchemaProperties>(entity: Entity, schema: S): InferConfig<S> {
66
+ const raw = entity.config?.value ?? {};
67
+ const result: Record<string, unknown> = {};
68
+ for (const [key, prop] of Object.entries(schema)) {
69
+ const val = raw[key];
70
+ result[key] = (val !== undefined && val !== null && typeof val === prop.type) ? val : prop.default;
71
+ }
72
+ return result as InferConfig<S>;
73
+ }
74
+
75
+ // Attach options
76
+
77
+ export type HealthResult = boolean | Record<number, { label: string; value: number | bigint }>;
78
+
79
+ export interface AttachOptions<S extends SchemaProperties> {
80
+ id: string;
81
+ label?: string;
82
+ controller?: string;
83
+ device?: { category?: string };
84
+ icon?: string;
85
+ schema: S;
86
+ run: (client: WorldClient, config: InferConfig<S>, signal: AbortSignal) => Promise<void>;
87
+ health?: () => HealthResult | Promise<HealthResult>;
88
+ interval?: number;
89
+ signal?: AbortSignal;
90
+ }
91
+
92
+ // Internals
93
+
94
+ const DEFAULT_INTERVAL = 10_000;
95
+
96
+ function isCanceled(err: unknown): boolean {
97
+ return err instanceof ConnectError && err.code === Code.Canceled;
98
+ }
99
+
100
+ function sleep(ms: number, signal: AbortSignal): Promise<void> {
101
+ return new Promise((resolve) => {
102
+ if (signal.aborted) return resolve();
103
+ const timer = setTimeout(resolve, ms);
104
+ signal.addEventListener("abort", () => { clearTimeout(timer); resolve(); }, { once: true });
105
+ });
106
+ }
107
+
108
+ /**
109
+ * Attach to a single device entity. Registers the entity, starts a heartbeat
110
+ * with TTL, watches for config changes, and runs the provided function.
111
+ *
112
+ * The health callback is polled every `interval` ms. Return `true` for healthy,
113
+ * `false` to skip the heartbeat (device expires via TTL), or a metrics map
114
+ * to push metrics alongside the heartbeat.
115
+ */
116
+ export async function attach<S extends SchemaProperties>(opts: AttachOptions<S>) {
117
+ const client = connect();
118
+ const entityID = opts.id;
119
+ const interval = opts.interval ?? DEFAULT_INTERVAL;
120
+ const health = opts.health ?? (() => true);
121
+
122
+ const entity = create(EntitySchema, {
123
+ id: entityID,
124
+ ...(opts.label && { label: opts.label }),
125
+ ...(opts.controller && { controller: create(ControllerSchema, { id: opts.controller }) }),
126
+ ...(opts.device && { device: create(DeviceComponentSchema, opts.device) }),
127
+ ...(opts.icon && { interactivity: create(InteractivityComponentSchema, { icon: opts.icon }) }),
128
+ configurable: create(ConfigurableComponentSchema, {
129
+ schema: { type: "object", properties: opts.schema as Record<string, unknown> },
130
+ }),
131
+ });
132
+
133
+ // Register entity without device — device is managed by heartbeat
134
+ await push(client, create(EntitySchema, { ...entity, device: undefined }));
135
+ console.log(`attached entity=${entityID}`);
136
+
137
+ // Heartbeat
138
+
139
+ let heartbeatId: ReturnType<typeof setInterval> | null = null;
140
+
141
+ const pushHeartbeat = (result?: Record<number, { label: string; value: number | bigint }>) => {
142
+ const e = create(EntitySchema, {
143
+ id: entityID,
144
+ device: create(DeviceComponentSchema, { ...entity.device, state: DeviceState.DeviceStateActive }),
145
+ lifetime: create(LifetimeSchema, {
146
+ until: create(TimestampSchema, { seconds: BigInt(Math.floor((Date.now() + interval + 1_000) / 1000)) }),
147
+ }),
148
+ });
149
+ if (result) {
150
+ e.metric = create(MetricComponentSchema, {
151
+ metrics: Object.entries(result).map(([idStr, { label, value }]) =>
152
+ typeof value === "bigint"
153
+ ? create(MetricSchema, { id: Number(idStr), label, kind: MetricKind.MetricKindCount, unit: MetricUnit.MetricUnitCount, val: { case: "uint64", value } })
154
+ : create(MetricSchema, { id: Number(idStr), label, kind: MetricKind.MetricKindGauge, unit: MetricUnit.MetricUnitNone, val: { case: "float", value } }),
155
+ ),
156
+ });
157
+ }
158
+ return push(client, e);
159
+ };
160
+
161
+ const outerAbort = new AbortController();
162
+ const tick = async () => {
163
+ if (outerAbort.signal.aborted) return;
164
+ try {
165
+ const r = await health();
166
+ if (r !== false) await pushHeartbeat(r === true ? undefined : r);
167
+ } catch { /* health check failed */ }
168
+ };
169
+ tick();
170
+ heartbeatId = setInterval(tick, interval);
171
+
172
+ // Config state management
173
+
174
+ const pushState = async (entity: Entity, state: ConfigurableState, error?: string) => {
175
+ const cfg = entity.configurable
176
+ ? clone(ConfigurableComponentSchema, entity.configurable)
177
+ : create(ConfigurableComponentSchema);
178
+ cfg.state = state;
179
+ cfg.error = error;
180
+ if (entity.config && state === ConfigurableState.ConfigurableStateActive) {
181
+ cfg.appliedVersion = entity.config.version;
182
+ }
183
+ await push(client, create(EntitySchema, { id: entityID, configurable: cfg })).catch(() => { });
184
+ };
185
+
186
+ let runningAbort: AbortController | null = null;
187
+ let currentConfigVersion = 0n;
188
+ let currentEntity: Entity | null = null;
189
+
190
+ const stop = () => {
191
+ if (runningAbort) {
192
+ const e = currentEntity;
193
+ runningAbort.abort();
194
+ runningAbort = null;
195
+ currentEntity = null;
196
+ console.log(`stopped entity=${entityID}`);
197
+ if (e) pushState(e, ConfigurableState.ConfigurableStateInactive);
198
+ }
199
+ };
200
+
201
+ const start = (e: Entity) => {
202
+ runningAbort = new AbortController();
203
+ currentEntity = e;
204
+ const childSignal = runningAbort.signal;
205
+
206
+ (async () => {
207
+ while (!childSignal.aborted) {
208
+ await pushState(e, ConfigurableState.ConfigurableStateStarting);
209
+ await pushState(e, ConfigurableState.ConfigurableStateActive);
210
+
211
+ try {
212
+ await opts.run(client, extractConfig(e, opts.schema), childSignal);
213
+ } catch (err) {
214
+ if (childSignal.aborted || isCanceled(err)) return;
215
+ console.error(`error, restarting entity=${entityID}`, err);
216
+ await pushState(e, ConfigurableState.ConfigurableStateFailed, String(err));
217
+ }
218
+
219
+ await sleep(5_000, childSignal);
220
+ }
221
+ })();
222
+ };
223
+
224
+ // Watch for config changes
225
+
226
+ const stream = client.watchEntities(
227
+ create(ListEntitiesRequestSchema, { filter: create(EntityFilterSchema, { id: entityID }) }),
228
+ ...(opts.signal ? [{ signal: opts.signal }] : []),
229
+ );
230
+
231
+ try {
232
+ for await (const event of stream) {
233
+ if (!event.entity) continue;
234
+
235
+ if (event.t === EntityChange.EntityChangeExpired || event.t === EntityChange.EntityChangeUnobserved) {
236
+ stop();
237
+ continue;
238
+ }
239
+ if (event.t !== EntityChange.EntityChangeUpdated) continue;
240
+
241
+ const e = event.entity;
242
+ if (!e.config) { stop(); continue; }
243
+ if (e.config.version === currentConfigVersion && runningAbort) continue;
244
+
245
+ stop();
246
+ currentConfigVersion = e.config.version;
247
+ start(e);
248
+ }
249
+ } catch (err) {
250
+ if (!isCanceled(err)) throw err;
251
+ } finally {
252
+ stop();
253
+ outerAbort.abort();
254
+ if (heartbeatId) clearInterval(heartbeatId);
255
+ }
256
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@projectqai/proto",
3
- "version": "0.0.25",
3
+ "version": "0.0.26",
4
4
  "author": "projectq-release-bot",
5
5
  "type": "module",
6
6
  "exports": {
@@ -8,15 +8,24 @@
8
8
  "types": "./dist/*_pb.d.ts",
9
9
  "import": "./dist/*_pb.js",
10
10
  "default": "./dist/*_pb.js"
11
+ },
12
+ "./device": {
13
+ "import": "./device.ts",
14
+ "default": "./device.ts"
11
15
  }
12
16
  },
13
17
  "files": [
14
- "dist"
18
+ "dist",
19
+ "device.ts"
15
20
  ],
16
21
  "license": "Apache-2.0",
17
22
  "peerDependencies": {
18
23
  "@bufbuild/protobuf": "^2.10.1"
19
24
  },
25
+ "dependencies": {
26
+ "@connectrpc/connect": "^2.0.0",
27
+ "@connectrpc/connect-node": "^2.0.0"
28
+ },
20
29
  "devDependencies": {
21
30
  "@bufbuild/protoc-gen-es": "^2.10.1"
22
31
  },
@@ -1,154 +0,0 @@
1
- // @generated by protoc-gen-es v2.10.2 with parameter "target=js+dts,import_extension=.js"
2
- // @generated from file controller.proto (package world, syntax proto3)
3
- /* eslint-disable */
4
-
5
- import type { GenEnum, GenFile, GenMessage, GenService } from "@bufbuild/protobuf/codegenv2";
6
- import type { Message } from "@bufbuild/protobuf";
7
- import type { Entity } from "./world_pb.js";
8
-
9
- /**
10
- * Describes the file controller.proto.
11
- */
12
- export declare const file_controller: GenFile;
13
-
14
- /**
15
- * @generated from message world.ControllerReconciliationRequest
16
- */
17
- export declare type ControllerReconciliationRequest = Message<"world.ControllerReconciliationRequest"> & {
18
- /**
19
- * @generated from field: string controller = 1;
20
- */
21
- controller: string;
22
- };
23
-
24
- /**
25
- * Describes the message world.ControllerReconciliationRequest.
26
- * Use `create(ControllerReconciliationRequestSchema)` to create a new message.
27
- */
28
- export declare const ControllerReconciliationRequestSchema: GenMessage<ControllerReconciliationRequest>;
29
-
30
- /**
31
- * An entity with Config on this controller was added, changed, or removed.
32
- *
33
- * @generated from message world.ControllerDeviceConfigurationEvent
34
- */
35
- export declare type ControllerDeviceConfigurationEvent = Message<"world.ControllerDeviceConfigurationEvent"> & {
36
- /**
37
- * @generated from field: world.ControllerDeviceConfigurationEventType t = 1;
38
- */
39
- t: ControllerDeviceConfigurationEventType;
40
-
41
- /**
42
- * @generated from field: world.Entity config = 2;
43
- */
44
- config?: Entity;
45
- };
46
-
47
- /**
48
- * Describes the message world.ControllerDeviceConfigurationEvent.
49
- * Use `create(ControllerDeviceConfigurationEventSchema)` to create a new message.
50
- */
51
- export declare const ControllerDeviceConfigurationEventSchema: GenMessage<ControllerDeviceConfigurationEvent>;
52
-
53
- /**
54
- * @generated from message world.ControllerReconciliationEvent
55
- */
56
- export declare type ControllerReconciliationEvent = Message<"world.ControllerReconciliationEvent"> & {
57
- /**
58
- * @generated from oneof world.ControllerReconciliationEvent.event
59
- */
60
- event: {
61
- /**
62
- * @generated from field: world.ControllerDeviceConfigurationEvent config = 2;
63
- */
64
- value: ControllerDeviceConfigurationEvent;
65
- case: "config";
66
- } | { case: undefined; value?: undefined };
67
- };
68
-
69
- /**
70
- * Describes the message world.ControllerReconciliationEvent.
71
- * Use `create(ControllerReconciliationEventSchema)` to create a new message.
72
- */
73
- export declare const ControllerReconciliationEventSchema: GenMessage<ControllerReconciliationEvent>;
74
-
75
- /**
76
- * @generated from message world.RestartConnectorRequest
77
- */
78
- export declare type RestartConnectorRequest = Message<"world.RestartConnectorRequest"> & {
79
- /**
80
- * @generated from field: string controller = 1;
81
- */
82
- controller: string;
83
-
84
- /**
85
- * @generated from field: string entity_id = 2;
86
- */
87
- entityId: string;
88
- };
89
-
90
- /**
91
- * Describes the message world.RestartConnectorRequest.
92
- * Use `create(RestartConnectorRequestSchema)` to create a new message.
93
- */
94
- export declare const RestartConnectorRequestSchema: GenMessage<RestartConnectorRequest>;
95
-
96
- /**
97
- * @generated from message world.RestartConnectorResponse
98
- */
99
- export declare type RestartConnectorResponse = Message<"world.RestartConnectorResponse"> & {
100
- };
101
-
102
- /**
103
- * Describes the message world.RestartConnectorResponse.
104
- * Use `create(RestartConnectorResponseSchema)` to create a new message.
105
- */
106
- export declare const RestartConnectorResponseSchema: GenMessage<RestartConnectorResponse>;
107
-
108
- /**
109
- * @generated from enum world.ControllerDeviceConfigurationEventType
110
- */
111
- export enum ControllerDeviceConfigurationEventType {
112
- /**
113
- * @generated from enum value: ControllerDeviceConfigurationEventNew = 0;
114
- */
115
- ControllerDeviceConfigurationEventNew = 0,
116
-
117
- /**
118
- * @generated from enum value: ControllerDeviceConfigurationEventChanged = 1;
119
- */
120
- ControllerDeviceConfigurationEventChanged = 1,
121
-
122
- /**
123
- * @generated from enum value: ControllerDeviceConfigurationEventRemoved = 2;
124
- */
125
- ControllerDeviceConfigurationEventRemoved = 2,
126
- }
127
-
128
- /**
129
- * Describes the enum world.ControllerDeviceConfigurationEventType.
130
- */
131
- export declare const ControllerDeviceConfigurationEventTypeSchema: GenEnum<ControllerDeviceConfigurationEventType>;
132
-
133
- /**
134
- * @generated from service world.ControllerService
135
- */
136
- export declare const ControllerService: GenService<{
137
- /**
138
- * @generated from rpc world.ControllerService.Reconcile
139
- */
140
- reconcile: {
141
- methodKind: "server_streaming";
142
- input: typeof ControllerReconciliationRequestSchema;
143
- output: typeof ControllerReconciliationEventSchema;
144
- },
145
- /**
146
- * @generated from rpc world.ControllerService.RestartConnector
147
- */
148
- restartConnector: {
149
- methodKind: "unary";
150
- input: typeof RestartConnectorRequestSchema;
151
- output: typeof RestartConnectorResponseSchema;
152
- },
153
- }>;
154
-
@@ -1,66 +0,0 @@
1
- // @generated by protoc-gen-es v2.10.2 with parameter "target=js+dts,import_extension=.js"
2
- // @generated from file controller.proto (package world, syntax proto3)
3
- /* eslint-disable */
4
-
5
- import { enumDesc, fileDesc, messageDesc, serviceDesc, tsEnum } from "@bufbuild/protobuf/codegenv2";
6
- import { file_world } from "./world_pb.js";
7
-
8
- /**
9
- * Describes the file controller.proto.
10
- */
11
- export const file_controller = /*@__PURE__*/
12
- fileDesc("ChBjb250cm9sbGVyLnByb3RvEgV3b3JsZCI1Ch9Db250cm9sbGVyUmVjb25jaWxpYXRpb25SZXF1ZXN0EhIKCmNvbnRyb2xsZXIYASABKAkifQoiQ29udHJvbGxlckRldmljZUNvbmZpZ3VyYXRpb25FdmVudBI4CgF0GAEgASgOMi0ud29ybGQuQ29udHJvbGxlckRldmljZUNvbmZpZ3VyYXRpb25FdmVudFR5cGUSHQoGY29uZmlnGAIgASgLMg0ud29ybGQuRW50aXR5ImUKHUNvbnRyb2xsZXJSZWNvbmNpbGlhdGlvbkV2ZW50EjsKBmNvbmZpZxgCIAEoCzIpLndvcmxkLkNvbnRyb2xsZXJEZXZpY2VDb25maWd1cmF0aW9uRXZlbnRIAEIHCgVldmVudCJAChdSZXN0YXJ0Q29ubmVjdG9yUmVxdWVzdBISCgpjb250cm9sbGVyGAEgASgJEhEKCWVudGl0eV9pZBgCIAEoCSIaChhSZXN0YXJ0Q29ubmVjdG9yUmVzcG9uc2UqsQEKJkNvbnRyb2xsZXJEZXZpY2VDb25maWd1cmF0aW9uRXZlbnRUeXBlEikKJUNvbnRyb2xsZXJEZXZpY2VDb25maWd1cmF0aW9uRXZlbnROZXcQABItCilDb250cm9sbGVyRGV2aWNlQ29uZmlndXJhdGlvbkV2ZW50Q2hhbmdlZBABEi0KKUNvbnRyb2xsZXJEZXZpY2VDb25maWd1cmF0aW9uRXZlbnRSZW1vdmVkEAIyxQEKEUNvbnRyb2xsZXJTZXJ2aWNlElsKCVJlY29uY2lsZRImLndvcmxkLkNvbnRyb2xsZXJSZWNvbmNpbGlhdGlvblJlcXVlc3QaJC53b3JsZC5Db250cm9sbGVyUmVjb25jaWxpYXRpb25FdmVudDABElMKEFJlc3RhcnRDb25uZWN0b3ISHi53b3JsZC5SZXN0YXJ0Q29ubmVjdG9yUmVxdWVzdBofLndvcmxkLlJlc3RhcnRDb25uZWN0b3JSZXNwb25zZUIgWh5naXRodWIuY29tL3Byb2plY3RxYWkvcHJvdG8vZ29iBnByb3RvMw", [file_world]);
13
-
14
- /**
15
- * Describes the message world.ControllerReconciliationRequest.
16
- * Use `create(ControllerReconciliationRequestSchema)` to create a new message.
17
- */
18
- export const ControllerReconciliationRequestSchema = /*@__PURE__*/
19
- messageDesc(file_controller, 0);
20
-
21
- /**
22
- * Describes the message world.ControllerDeviceConfigurationEvent.
23
- * Use `create(ControllerDeviceConfigurationEventSchema)` to create a new message.
24
- */
25
- export const ControllerDeviceConfigurationEventSchema = /*@__PURE__*/
26
- messageDesc(file_controller, 1);
27
-
28
- /**
29
- * Describes the message world.ControllerReconciliationEvent.
30
- * Use `create(ControllerReconciliationEventSchema)` to create a new message.
31
- */
32
- export const ControllerReconciliationEventSchema = /*@__PURE__*/
33
- messageDesc(file_controller, 2);
34
-
35
- /**
36
- * Describes the message world.RestartConnectorRequest.
37
- * Use `create(RestartConnectorRequestSchema)` to create a new message.
38
- */
39
- export const RestartConnectorRequestSchema = /*@__PURE__*/
40
- messageDesc(file_controller, 3);
41
-
42
- /**
43
- * Describes the message world.RestartConnectorResponse.
44
- * Use `create(RestartConnectorResponseSchema)` to create a new message.
45
- */
46
- export const RestartConnectorResponseSchema = /*@__PURE__*/
47
- messageDesc(file_controller, 4);
48
-
49
- /**
50
- * Describes the enum world.ControllerDeviceConfigurationEventType.
51
- */
52
- export const ControllerDeviceConfigurationEventTypeSchema = /*@__PURE__*/
53
- enumDesc(file_controller, 0);
54
-
55
- /**
56
- * @generated from enum world.ControllerDeviceConfigurationEventType
57
- */
58
- export const ControllerDeviceConfigurationEventType = /*@__PURE__*/
59
- tsEnum(ControllerDeviceConfigurationEventTypeSchema);
60
-
61
- /**
62
- * @generated from service world.ControllerService
63
- */
64
- export const ControllerService = /*@__PURE__*/
65
- serviceDesc(file_controller, 0);
66
-