@vampgg/utils 1.0.0-beta.4 → 1.0.0-beta.6

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 CHANGED
@@ -19,7 +19,7 @@ vp run build # regenerate schema then build (runs: bebopc build && vp pa
19
19
 
20
20
  ## Schema (`schema/`)
21
21
 
22
- Two bebop types define the wire format for all RPC in this framework.
22
+ Two bebop types define the wire format for all RPC in this framework. The remaining files are entity components that apps import into their own schema.
23
23
 
24
24
  ### `Error` (struct — `schema/error.bop`)
25
25
 
@@ -50,6 +50,37 @@ The universal envelope for all RPC calls. Fields are optional (bebop message typ
50
50
  | `credential` | `string` | Serialized credential for storage |
51
51
  | `headers` | `string` | HTTP-style metadata header string |
52
52
 
53
+ ### Behavior trees (`schema/behavior.bop`)
54
+
55
+ Entity components for `@vampgg/ecs` behavior trees. Import the file from your
56
+ `entity.bop` (as `vamp init` does), then add `Brain` and `BehaviorTree` fields to
57
+ `Entity`. `vamp generate` merges both of their deltas as last-write-wins, never
58
+ as counters.
59
+
60
+ | Type | Fields | Role |
61
+ | ------------------ | --------------------------------------------------------------------------------------- | --------------------------------------------------------- |
62
+ | `BehaviorNodeKind` | `Selector`, `Sequence`, `Weighted`, `Condition`, `Task`, `Invert`, `Chance`, `Cooldown` | What a node does |
63
+ | `BehaviorNode` | `kind`, `children`, `weight`, `leaf`, `args` | One node; `children` are indices into `nodes` |
64
+ | `BehaviorTree` | `nodes` | A flat tree whose root is `nodes[0]`, stored on an entity |
65
+ | `Brain` | `tree`, `readyAt`, `last` | An agent's tree id, cooldown expiry ticks, and last task |
66
+
67
+ `weight` is a `Chance` percentage or a `Cooldown` length in ticks. A `Weighted`
68
+ node keeps its per-child weights in `args`. `leaf` is the app's `Condition` or
69
+ `Task` enum value.
70
+
71
+ ### Vectors (`schema/vec.bop`)
72
+
73
+ Spatial entity components. Import the file from your `entity.bop` (as `vamp init`
74
+ does), then add `Vec2` or `Vec3` fields to `Entity`. `vamp generate` merges their
75
+ deltas as counters, so a delta of `{ x: 0.5 }` adds `0.5` to `x`.
76
+
77
+ | Type | Fields | Role |
78
+ | ----------- | ------------- | ------------------------------------ |
79
+ | `Vec2` | `x`, `y` | A 2D vector, such as a position |
80
+ | `Vec2Delta` | `x`, `y` | Signed `float32` offsets onto a Vec2 |
81
+ | `Vec3` | `x`, `y`, `z` | A 3D vector |
82
+ | `Vec3Delta` | `x`, `y`, `z` | Signed `float32` offsets onto a Vec3 |
83
+
53
84
  ---
54
85
 
55
86
  ## Subpath Exports
@@ -52,12 +52,12 @@ declare class AsyncQueue<T> implements AsyncIterableIterator<T> {
52
52
  /** Producer: terminate the stream as a clean completion. Idempotent. */
53
53
  close(): void;
54
54
  /** Producer: terminate the stream with an error thrown into the consumer. Idempotent. */
55
- fail(error: unknown): void;
55
+ fail(cause: unknown): void;
56
56
  next(): Promise<IteratorResult<T>>;
57
57
  /** Consumer aborted early (break / return). Runs dispose and ends the stream. */
58
- return(value?: unknown): Promise<IteratorResult<T>>;
58
+ return(value?: undefined): Promise<IteratorResult<T>>;
59
59
  /** Consumer threw. Propagate after dispose. */
60
- throw(e?: unknown): Promise<IteratorResult<T>>;
60
+ throw(cause?: unknown): Promise<IteratorResult<T>>;
61
61
  [Symbol.asyncIterator](): AsyncIterableIterator<T>;
62
62
  private enqueue;
63
63
  private dequeue;
@@ -62,15 +62,15 @@ var AsyncQueue = class {
62
62
  }
63
63
  }
64
64
  /** Producer: terminate the stream with an error thrown into the consumer. Idempotent. */
65
- fail(error) {
65
+ fail(cause) {
66
66
  if (this.closed || this.hasError) return;
67
67
  this.hasError = true;
68
- this.error = error;
68
+ this.error = cause;
69
69
  if (this.pendingReject) {
70
70
  const reject = this.pendingReject;
71
71
  this.pendingResolve = null;
72
72
  this.pendingReject = null;
73
- reject(error);
73
+ reject(cause);
74
74
  }
75
75
  }
76
76
  async next() {
@@ -113,16 +113,16 @@ var AsyncQueue = class {
113
113
  };
114
114
  }
115
115
  /** Consumer threw. Propagate after dispose. */
116
- async throw(e) {
116
+ async throw(cause) {
117
117
  this.closed = true;
118
118
  if (this.pendingReject) {
119
119
  const reject = this.pendingReject;
120
120
  this.pendingResolve = null;
121
121
  this.pendingReject = null;
122
- reject(e);
122
+ reject(cause);
123
123
  }
124
124
  await this.dispose();
125
- throw e;
125
+ throw cause;
126
126
  }
127
127
  [Symbol.asyncIterator]() {
128
128
  return this;
@@ -28,7 +28,7 @@ declare abstract class CoreChannel extends BaseChannel {
28
28
  * Reject callbacks for in-flight unary / client-stream calls, keyed by
29
29
  * messageId. Lets close()/error() settle parked requests instead of hanging.
30
30
  */
31
- readonly pending: Map<string, (e: unknown) => void>;
31
+ readonly pending: Map<string, (cause: unknown) => void>;
32
32
  protected closed: boolean;
33
33
  /** The reason a closed channel settles calls that arrive after teardown. */
34
34
  protected closeReason?: TempoError;
@@ -39,7 +39,7 @@ declare abstract class CoreChannel extends BaseChannel {
39
39
  * synchronous (worker / WebSocket) or asynchronous (extension runtime); the
40
40
  * call sites await or attach rejection handlers as each RPC shape requires.
41
41
  */
42
- protected abstract sendFrame(message: Message): void | Promise<unknown>;
42
+ protected abstract sendFrame(message: Message): void | Promise<void>;
43
43
  /** Generates the unique id for an outgoing request envelope. */
44
44
  protected generateMessageId(): string;
45
45
  /**
@@ -308,14 +308,14 @@ var CoreChannel = class extends BaseChannel {
308
308
  * Runs the shared error-hook + error-mapping tail of every `start*` method.
309
309
  * Always throws.
310
310
  */
311
- async raiseCallError(context, e) {
312
- if (this.hooks !== void 0 && e instanceof Error) await this.hooks.executeErrorHooks(context, e);
313
- if (e instanceof TempoError) throw e;
314
- if (e instanceof Error) {
315
- if (e.name === "AbortError") throw new TempoError(TempoStatusCode.ABORTED, "RPC fetch aborted", e);
316
- throw new TempoError(TempoStatusCode.UNKNOWN, "an unknown error occurred", e);
311
+ async raiseCallError(context, cause) {
312
+ if (this.hooks !== void 0 && cause instanceof Error) await this.hooks.executeErrorHooks(context, cause);
313
+ if (cause instanceof TempoError) throw cause;
314
+ if (cause instanceof Error) {
315
+ if (cause.name === "AbortError") throw new TempoError(TempoStatusCode.ABORTED, "RPC fetch aborted", cause);
316
+ throw new TempoError(TempoStatusCode.UNKNOWN, "an unknown error occurred", cause);
317
317
  }
318
- throw new TempoError(TempoStatusCode.UNKNOWN, "an unknown error occurred", { data: e });
318
+ throw new TempoError(TempoStatusCode.UNKNOWN, "an unknown error occurred", { data: cause });
319
319
  }
320
320
  /**
321
321
  * {@inheritDoc BaseChannel.startUnary}
@@ -1,11 +1,13 @@
1
1
  import { ConsoleLogger, TempoLogLevel, TempoLogger } from "@tempojs/common";
2
2
 
3
3
  //#region src/context-logger.d.ts
4
+ /** Structured log fields, as TempoLogger's own `write` contract defines them. */
5
+ type LogData = NonNullable<Parameters<TempoLogger["write"]>[2]>;
4
6
  declare class ContextLogger extends ConsoleLogger {
5
- bindings: Record<string, unknown>;
6
- constructor(sourceName: string, logLevel?: TempoLogLevel, parent?: TempoLogger, bindings?: Record<string, unknown>);
7
- correlate<TLogger extends TempoLogger>(sourceName: string, correlationId?: string, asOrphan?: boolean, bindings?: Record<string, unknown>): TLogger;
8
- clone<TLogger extends TempoLogger>(sourceName: string, asOrphan?: boolean, bindings?: Record<string, unknown>): TLogger;
7
+ bindings: LogData;
8
+ constructor(sourceName: string, logLevel?: TempoLogLevel, parent?: TempoLogger, bindings?: LogData);
9
+ correlate<TLogger extends TempoLogger>(sourceName: string, correlationId?: string, asOrphan?: boolean, bindings?: LogData): TLogger;
10
+ clone<TLogger extends TempoLogger>(sourceName: string, asOrphan?: boolean, bindings?: LogData): TLogger;
9
11
  }
10
12
  //#endregion
11
- export { ContextLogger };
13
+ export { ContextLogger, LogData };
@@ -8,10 +8,9 @@ var ContextLogger = class extends ConsoleLogger {
8
8
  TempoLogger.instances.delete(sourceName.replace(/\s+/g, "_"));
9
9
  }
10
10
  correlate(sourceName, correlationId, asOrphan, bindings) {
11
- return this.clone(sourceName, asOrphan, {
12
- ...correlationId ? { correlationId } : {},
13
- ...bindings
14
- });
11
+ const merged = correlationId ? { correlationId } : {};
12
+ Object.assign(merged, bindings);
13
+ return this.clone(sourceName, asOrphan, merged);
15
14
  }
16
15
  clone(sourceName, asOrphan, bindings) {
17
16
  const logger = Reflect.construct(this.constructor, [
@@ -4,7 +4,7 @@ import { AsyncQueue, AsyncQueueOptions } from "./async-queue.mjs";
4
4
  type Context<T> = {
5
5
  emit: (value: T) => void; /** Terminate the stream as a clean completion. */
6
6
  cancel: () => void; /** Terminate the stream with an error thrown into the consumer's `for await`. */
7
- error: (e: unknown) => void;
7
+ error: (cause: unknown) => void;
8
8
  };
9
9
  type CleanupFn = () => void | Promise<void>;
10
10
  type Subscriber<T> = (context: Context<T>) => void | CleanupFn | Promise<CleanupFn | void>;
@@ -34,9 +34,9 @@ function queueIterator(queue, start) {
34
34
  if (started !== void 0) await started.catch(() => {});
35
35
  return queue.return(value);
36
36
  },
37
- async throw(e) {
37
+ async throw(cause) {
38
38
  if (started !== void 0) await started.catch(() => {});
39
- return queue.throw(e);
39
+ return queue.throw(cause);
40
40
  },
41
41
  [Symbol.asyncIterator]() {
42
42
  return this;
@@ -57,7 +57,7 @@ function createEventIterator(subscriber, options) {
57
57
  queue.push(value);
58
58
  },
59
59
  cancel: () => queue.close(),
60
- error: (e) => queue.fail(e)
60
+ error: (cause) => queue.fail(cause)
61
61
  };
62
62
  return queueIterator(queue, async () => {
63
63
  unsubscribe = await subscriber(context);
@@ -33,8 +33,8 @@ declare class TempoExtensionChannel extends CoreChannel {
33
33
  /**
34
34
  * {@inheritDoc CoreChannel.sendFrame}
35
35
  */
36
- protected sendFrame(message: Message): Promise<unknown>;
37
- protected sendMessage(message: Message): Promise<unknown>;
36
+ protected sendFrame(message: Message): Promise<void>;
37
+ protected sendMessage(message: Message): Promise<void>;
38
38
  }
39
39
  //#endregion
40
40
  export { TempoExtensionChannel };
@@ -159,6 +159,6 @@ type ExtensionSenderContext = {
159
159
  * ignored. Replies travel back through the router's send path, so this never
160
160
  * uses `sendResponse`.
161
161
  */
162
- declare function createExtensionListener(router: Pick<TempoExtensionRouter<ExtensionSenderContext>, "process">, registry: ServiceRegistry): (raw: unknown, sender: Runtime.MessageSender) => undefined;
162
+ declare function createExtensionListener(router: Pick<TempoExtensionRouter<ExtensionSenderContext>, "process">, registry: ServiceRegistry): Runtime.OnMessageListenerNoResponse;
163
163
  //#endregion
164
164
  export { ExtensionBaseRouter, ExtensionSenderContext, TempoExtensionRouter, TempoExtensionRouterConfiguration, createExtensionListener };
@@ -1,4 +1,4 @@
1
- import { ContextLogger } from "./context-logger.mjs";
1
+ import { ContextLogger, LogData } from "./context-logger.mjs";
2
2
  import { ConsoleLogger, TempoLogLevel, TempoLogger } from "@tempojs/common";
3
3
  import pino from "pino";
4
4
 
@@ -8,34 +8,34 @@ declare class PinoLogger extends ContextLogger implements ConsoleLogger {
8
8
  sourceName: string;
9
9
  logLevel: TempoLogLevel;
10
10
  children: Set<TempoLogger>;
11
- constructor(sourceName: string, logLevel?: TempoLogLevel, parent?: pino.Logger, bindings?: Record<string, unknown>);
11
+ constructor(sourceName: string, logLevel?: TempoLogLevel, parent?: pino.Logger, bindings?: LogData);
12
12
  static getPinoLevel(level: string): string;
13
- clone<TLogger extends TempoLogger>(sourceName: string, asOrphan?: boolean, bindings?: Record<string, unknown>): TLogger;
14
- write(level: TempoLogLevel, message: string, data?: Record<string, unknown>, error?: Error): void;
13
+ clone<TLogger extends TempoLogger>(sourceName: string, asOrphan?: boolean, bindings?: LogData): TLogger;
14
+ write(level: TempoLogLevel, message: string, data?: LogData, error?: Error): void;
15
15
  /**
16
16
  * @inheritDoc
17
17
  */
18
- trace(message: string, data?: Record<string, unknown>, error?: Error): void;
18
+ trace(message: string, data?: LogData, error?: Error): void;
19
19
  /**
20
20
  * @inheritDoc
21
21
  */
22
- debug(message: string, data?: Record<string, unknown>, error?: Error): void;
22
+ debug(message: string, data?: LogData, error?: Error): void;
23
23
  /**
24
24
  * @inheritDoc
25
25
  */
26
- info(message: string, data?: Record<string, unknown>, error?: Error): void;
26
+ info(message: string, data?: LogData, error?: Error): void;
27
27
  /**
28
28
  * @inheritDoc
29
29
  */
30
- warn(message: string, data?: Record<string, unknown>, error?: Error): void;
30
+ warn(message: string, data?: LogData, error?: Error): void;
31
31
  /**
32
32
  * @inheritDoc
33
33
  */
34
- error(message: string, data?: Record<string, unknown>, error?: Error): void;
34
+ error(message: string, data?: LogData, error?: Error): void;
35
35
  /**
36
36
  * @inheritDoc
37
37
  */
38
- critical(message: string, data?: Record<string, unknown>, error?: Error): void;
38
+ critical(message: string, data?: LogData, error?: Error): void;
39
39
  setLogLevel(level: TempoLogLevel): void;
40
40
  private tempoPinoLevelMap;
41
41
  formatErrorMessage(error: Error, indentLevel?: number): string;
@@ -115,7 +115,7 @@ declare class RouterCore {
115
115
  * type, populate and deliver the response, and map any failure onto an error
116
116
  * frame. Transport-specific behavior is injected via `transport`.
117
117
  */
118
- processRequest(request: Message, response: Message, env: unknown, transport: ProcessTransport, contentType?: BebopContentType): Promise<void>;
118
+ processRequest<TEnv>(request: Message, response: Message, env: TEnv, transport: ProcessTransport, contentType?: BebopContentType): Promise<void>;
119
119
  }
120
120
  //#endregion
121
121
  export { ProcessTransport, RouterCore, RouterCoreHost, StreamWriteOptions };
@@ -0,0 +1,21 @@
1
+ import { TempoLogger } from "@tempojs/common";
2
+ import { BebopMethodAny, ServiceRegistry } from "@tempojs/server";
3
+
4
+ //#region src/service-registry.d.ts
5
+ /**
6
+ * One router takes one registry, but a host can generate services across
7
+ * several packages (each with its own registry class). Method ids are
8
+ * service+method hashes (collision-free across services), so first-hit-wins
9
+ * lookup is safe. Generated `init()` is destructive (moves the
10
+ * decorator-registered singleton out of a static map), so it must run exactly
11
+ * once — the router constructor also calls `init()`.
12
+ */
13
+ declare class CompositeServiceRegistry extends ServiceRegistry {
14
+ #private;
15
+ private readonly registries;
16
+ constructor(logger: TempoLogger, registries: readonly ServiceRegistry[]);
17
+ init(): void;
18
+ getMethod(methodId: number): BebopMethodAny | undefined;
19
+ }
20
+ //#endregion
21
+ export { CompositeServiceRegistry };
@@ -0,0 +1,31 @@
1
+ import { ServiceRegistry } from "@tempojs/server";
2
+ //#region src/service-registry.ts
3
+ /**
4
+ * One router takes one registry, but a host can generate services across
5
+ * several packages (each with its own registry class). Method ids are
6
+ * service+method hashes (collision-free across services), so first-hit-wins
7
+ * lookup is safe. Generated `init()` is destructive (moves the
8
+ * decorator-registered singleton out of a static map), so it must run exactly
9
+ * once — the router constructor also calls `init()`.
10
+ */
11
+ var CompositeServiceRegistry = class extends ServiceRegistry {
12
+ registries;
13
+ #initialized = false;
14
+ constructor(logger, registries) {
15
+ super(logger);
16
+ this.registries = registries;
17
+ }
18
+ init() {
19
+ if (this.#initialized) return;
20
+ this.#initialized = true;
21
+ for (const registry of this.registries) registry.init();
22
+ }
23
+ getMethod(methodId) {
24
+ for (const registry of this.registries) {
25
+ const method = registry.getMethod(methodId);
26
+ if (method) return method;
27
+ }
28
+ }
29
+ };
30
+ //#endregion
31
+ export { CompositeServiceRegistry };
@@ -79,7 +79,7 @@ var TempoWSChannel = class TempoWSChannel extends CoreChannel {
79
79
  */
80
80
  static forAddress(address, options) {
81
81
  if (!address) throw new Error("no address");
82
- if (typeof address === "string") address = new URL(address);
82
+ if (!(address instanceof URL)) address = new URL(address);
83
83
  options ??= {};
84
84
  return new TempoWSChannel(address, options);
85
85
  }
@@ -102,14 +102,15 @@ var TempoWSChannel = class TempoWSChannel extends CoreChannel {
102
102
  this.close(new TempoError(TempoStatusCode.UNAVAILABLE, "channel transport error"));
103
103
  });
104
104
  this.ws.addEventListener(WebsocketEvent.message, (_ws, ev) => {
105
- const byteLength = typeof ev.data === "string" ? ev.data.length : ev.data.byteLength;
105
+ const data = ev.data;
106
+ const byteLength = data instanceof ArrayBuffer ? data.byteLength : data.length;
106
107
  if (byteLength > this.maxReceiveMessageSize) {
107
108
  this.logger.error(`inbound frame ${byteLength}B exceeds maxReceiveMessageSize ${this.maxReceiveMessageSize}B; dropping`);
108
109
  return;
109
110
  }
110
111
  let message;
111
- if (typeof ev.data === "string") message = Message(JSON.parse(ev.data));
112
- else message = Message(Message.decode(new Uint8Array(ev.data)));
112
+ if (data instanceof ArrayBuffer) message = Message(Message.decode(new Uint8Array(data)));
113
+ else message = Message(JSON.parse(data));
113
114
  const messageId = message.messageId;
114
115
  this.events.emit(messageId, message);
115
116
  this.logger.trace(`received new message ${messageId}`);
@@ -168,8 +169,7 @@ var TempoWSChannel = class TempoWSChannel extends CoreChannel {
168
169
  };
169
170
  const onError = (ws, event) => {
170
171
  teardown();
171
- const message = event.message;
172
- const detail = typeof message === "string" ? message : event.type;
172
+ const detail = event.message ?? event.type;
173
173
  reject(new TempoError(TempoStatusCode.UNAVAILABLE, `socket error before open: ${detail}`));
174
174
  };
175
175
  const onClose = () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vampgg/utils",
3
- "version": "1.0.0-beta.4",
3
+ "version": "1.0.0-beta.6",
4
4
  "description": "Tempo RPC, Bebop serialization, transports, and async primitives for @vampgg.",
5
5
  "homepage": "https://github.com/sammccord/vamp/tree/main/packages/utils#readme",
6
6
  "bugs": {
@@ -29,6 +29,7 @@
29
29
  "./extension-router": "./dist/extension-router.mjs",
30
30
  "./pino-logger": "./dist/pino-logger.mjs",
31
31
  "./router-core": "./dist/router-core.mjs",
32
+ "./service-registry": "./dist/service-registry.mjs",
32
33
  "./worker-channel": "./dist/worker-channel.mjs",
33
34
  "./worker-router": "./dist/worker-router.mjs",
34
35
  "./ws-channel": "./dist/ws-channel.mjs",
@@ -0,0 +1,45 @@
1
+ // Behavior tree built-ins. A BehaviorTree lives on its own entity and is shared
2
+ // by every agent whose Brain points at it. Nodes form a flat array linked by
3
+ // index; nodes[0] is the root.
4
+ enum BehaviorNodeKind {
5
+ Selector = 1; // first child that succeeds
6
+ Sequence = 2; // every child succeeds, in order
7
+ Weighted = 3; // children that pass, picked by weight
8
+ Condition = 4; // leaf: registered predicate
9
+ Task = 5; // leaf: registered task, emits action intents
10
+ Invert = 6;
11
+ Chance = 7; // decorator: passes with probability weight/100
12
+ Cooldown = 8; // decorator: fails until `weight` ticks since its last success
13
+ }
14
+
15
+ message BehaviorNode {
16
+ 1 -> BehaviorNodeKind kind;
17
+ 2 -> uint16[] children; // indices into BehaviorTree.nodes
18
+ 3 -> uint32 weight; // Chance percent or Cooldown ticks
19
+ 4 -> uint32 leaf; // Condition / Task id (app enum)
20
+ 5 -> float32[] args; // leaf arguments; per-child weights on Weighted
21
+ }
22
+
23
+ message BehaviorTree {
24
+ 1 -> BehaviorNode[] nodes;
25
+ }
26
+
27
+ // Behavior tree delta - last-write-wins replace of the whole node array
28
+ message BehaviorTreeDelta {
29
+ 1 -> BehaviorNode[] nodes;
30
+ }
31
+
32
+ message Brain {
33
+ 1 -> string tree; // id of the entity that holds the BehaviorTree
34
+ 2 -> uint32[] readyAt; // per-node cooldown expiry tick
35
+ 3 -> uint16 last; // last task node that fired
36
+ 4 -> uint32 fingerprint; // of the tree that readyAt and last index into
37
+ }
38
+
39
+ // Brain delta - every field is last-write-wins
40
+ message BrainDelta {
41
+ 1 -> string tree;
42
+ 2 -> uint32[] readyAt;
43
+ 3 -> uint16 last;
44
+ 4 -> uint32 fingerprint;
45
+ }
package/schema/pool.bop CHANGED
@@ -3,7 +3,7 @@ message Pool {
3
3
  2 -> uint32 min;
4
4
  3 -> uint32 max;
5
5
  4 -> int32 rate;
6
- 5 -> uint32 interval;
6
+ 5 -> uint32 interval;
7
7
  }
8
8
 
9
9
  // Pool delta - points is CRDT counter, others are last-write-wins
package/schema/vec.bop ADDED
@@ -0,0 +1,24 @@
1
+ // Vector built-ins for positions, velocities, and other spatial components.
2
+ message Vec2 {
3
+ 1 -> float32 x;
4
+ 2 -> float32 y;
5
+ }
6
+
7
+ // Vec2 delta - every field is a float CRDT counter (+/-)
8
+ message Vec2Delta {
9
+ 1 -> float32 x;
10
+ 2 -> float32 y;
11
+ }
12
+
13
+ message Vec3 {
14
+ 1 -> float32 x;
15
+ 2 -> float32 y;
16
+ 3 -> float32 z;
17
+ }
18
+
19
+ // Vec3 delta - every field is a float CRDT counter (+/-)
20
+ message Vec3Delta {
21
+ 1 -> float32 x;
22
+ 2 -> float32 y;
23
+ 3 -> float32 z;
24
+ }