@synnaxlabs/client 0.26.7 → 0.28.0

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.
Files changed (140) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/README.md +36 -10
  3. package/api/client.api.md +3121 -0
  4. package/api-extractor.json +7 -0
  5. package/dist/access/client.d.ts +0 -1
  6. package/dist/access/payload.d.ts +0 -1
  7. package/dist/auth/auth.d.ts +0 -1
  8. package/dist/channel/client.d.ts +6 -2
  9. package/dist/channel/client.d.ts.map +1 -1
  10. package/dist/channel/creator.d.ts +0 -1
  11. package/dist/channel/payload.d.ts +4 -1
  12. package/dist/channel/payload.d.ts.map +1 -1
  13. package/dist/channel/retriever.d.ts +0 -1
  14. package/dist/channel/writer.d.ts +0 -1
  15. package/dist/client.cjs +23 -19
  16. package/dist/client.d.ts +10 -7
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +2326 -1904
  19. package/dist/connection/checker.d.ts +21 -2
  20. package/dist/connection/checker.d.ts.map +1 -1
  21. package/dist/control/client.d.ts +0 -1
  22. package/dist/control/client.d.ts.map +1 -1
  23. package/dist/control/state.d.ts +0 -1
  24. package/dist/errors.d.ts +0 -1
  25. package/dist/framer/adapter.d.ts +0 -1
  26. package/dist/framer/client.d.ts +0 -1
  27. package/dist/framer/client.d.ts.map +1 -1
  28. package/dist/framer/deleter.d.ts +0 -1
  29. package/dist/framer/frame.d.ts +11 -12
  30. package/dist/framer/iterator.d.ts +0 -1
  31. package/dist/framer/streamProxy.d.ts +0 -1
  32. package/dist/framer/streamer.d.ts +0 -1
  33. package/dist/framer/writer.d.ts +0 -1
  34. package/dist/framer/writer.d.ts.map +1 -1
  35. package/dist/hardware/client.d.ts +0 -1
  36. package/dist/hardware/device/client.d.ts +0 -1
  37. package/dist/hardware/device/payload.d.ts +0 -1
  38. package/dist/hardware/device/payload.d.ts.map +1 -1
  39. package/dist/hardware/rack/client.d.ts +0 -1
  40. package/dist/hardware/rack/payload.d.ts +0 -1
  41. package/dist/hardware/rack/payload.d.ts.map +1 -1
  42. package/dist/hardware/task/client.d.ts +12 -3
  43. package/dist/hardware/task/client.d.ts.map +1 -1
  44. package/dist/hardware/task/ni/types.d.ts +14495 -0
  45. package/dist/hardware/task/ni/types.d.ts.map +1 -0
  46. package/dist/hardware/task/payload.d.ts +6 -1
  47. package/dist/hardware/task/payload.d.ts.map +1 -1
  48. package/dist/label/client.d.ts +8 -6
  49. package/dist/label/client.d.ts.map +1 -1
  50. package/dist/label/payload.d.ts +0 -1
  51. package/dist/label/retriever.d.ts +0 -1
  52. package/dist/label/retriever.d.ts.map +1 -1
  53. package/dist/label/writer.d.ts +32 -2
  54. package/dist/label/writer.d.ts.map +1 -1
  55. package/dist/ontology/client.d.ts +136 -12
  56. package/dist/ontology/client.d.ts.map +1 -1
  57. package/dist/ontology/group/client.d.ts +0 -1
  58. package/dist/ontology/group/group.d.ts +0 -1
  59. package/dist/ontology/group/payload.d.ts +0 -1
  60. package/dist/ontology/group/writer.d.ts +0 -1
  61. package/dist/ontology/group/writer.d.ts.map +1 -1
  62. package/dist/ontology/payload.d.ts +6 -3
  63. package/dist/ontology/payload.d.ts.map +1 -1
  64. package/dist/ontology/writer.d.ts +4 -5
  65. package/dist/ontology/writer.d.ts.map +1 -1
  66. package/dist/ranger/alias.d.ts +0 -1
  67. package/dist/ranger/client.d.ts +57 -14
  68. package/dist/ranger/client.d.ts.map +1 -1
  69. package/dist/ranger/external.d.ts +1 -1
  70. package/dist/ranger/external.d.ts.map +1 -1
  71. package/dist/ranger/kv.d.ts +42 -5
  72. package/dist/ranger/kv.d.ts.map +1 -1
  73. package/dist/ranger/payload.d.ts +5 -2
  74. package/dist/ranger/payload.d.ts.map +1 -1
  75. package/dist/ranger/writer.d.ts +107 -2
  76. package/dist/ranger/writer.d.ts.map +1 -1
  77. package/dist/setupspecs.d.ts +0 -1
  78. package/dist/signals/observable.d.ts +0 -1
  79. package/dist/transport.d.ts +0 -1
  80. package/dist/user/client.d.ts +0 -1
  81. package/dist/user/payload.d.ts +0 -1
  82. package/dist/util/retrieve.d.ts +0 -1
  83. package/dist/util/telem.d.ts +0 -1
  84. package/dist/util/zod.d.ts +0 -1
  85. package/dist/workspace/client.d.ts +0 -1
  86. package/dist/workspace/lineplot/client.d.ts +0 -1
  87. package/dist/workspace/lineplot/payload.d.ts +0 -1
  88. package/dist/workspace/lineplot/retriever.d.ts +0 -1
  89. package/dist/workspace/lineplot/writer.d.ts +0 -1
  90. package/dist/workspace/payload.d.ts +0 -1
  91. package/dist/workspace/retriever.d.ts +0 -1
  92. package/dist/workspace/schematic/client.d.ts +0 -1
  93. package/dist/workspace/schematic/payload.d.ts +0 -1
  94. package/dist/workspace/schematic/retriever.d.ts +0 -1
  95. package/dist/workspace/schematic/writer.d.ts +0 -1
  96. package/dist/workspace/writer.d.ts +0 -1
  97. package/package.json +14 -12
  98. package/src/access/access.spec.ts +11 -11
  99. package/src/channel/batchRetriever.spec.ts +2 -0
  100. package/src/channel/channel.spec.ts +51 -31
  101. package/src/channel/client.ts +7 -0
  102. package/src/channel/payload.ts +1 -0
  103. package/src/client.ts +17 -8
  104. package/src/connection/checker.ts +58 -1
  105. package/src/connection/connection.spec.ts +43 -3
  106. package/src/control/client.ts +9 -0
  107. package/src/errors.spec.ts +9 -0
  108. package/src/framer/client.ts +0 -1
  109. package/src/framer/frame.spec.ts +2 -2
  110. package/src/framer/frame.ts +22 -22
  111. package/src/framer/writer.ts +2 -1
  112. package/src/hardware/device/payload.ts +9 -0
  113. package/src/hardware/rack/payload.ts +9 -0
  114. package/src/hardware/task/client.ts +82 -6
  115. package/src/hardware/task/ni/types.ts +1716 -0
  116. package/src/hardware/task/payload.ts +10 -0
  117. package/src/hardware/task/task.spec.ts +45 -30
  118. package/src/label/client.ts +49 -19
  119. package/src/label/label.spec.ts +9 -0
  120. package/src/label/retriever.ts +2 -1
  121. package/src/label/writer.ts +11 -3
  122. package/src/ontology/client.ts +227 -14
  123. package/src/ontology/group/writer.ts +10 -12
  124. package/src/ontology/ontology.spec.ts +3 -5
  125. package/src/ontology/payload.ts +5 -1
  126. package/src/ontology/writer.ts +26 -12
  127. package/src/ranger/client.ts +223 -41
  128. package/src/ranger/external.ts +1 -1
  129. package/src/ranger/kv.ts +50 -11
  130. package/src/ranger/payload.ts +9 -5
  131. package/src/ranger/ranger.spec.ts +114 -49
  132. package/src/ranger/writer.ts +7 -2
  133. package/src/vite-env.d.ts +1 -1
  134. package/vite.config.ts +6 -1
  135. package/dist/ranger/active.d.ts +0 -11
  136. package/dist/ranger/active.d.ts.map +0 -1
  137. package/dist/ranger/range.d.ts +0 -32
  138. package/dist/ranger/range.d.ts.map +0 -1
  139. package/src/ranger/active.ts +0 -74
  140. package/src/ranger/range.ts +0 -98
@@ -23,7 +23,7 @@ const client = newClient();
23
23
 
24
24
  describe("Policy", () => {
25
25
  describe("create", () => {
26
- test("create one", async () => {
26
+ test.skip("create one", async () => {
27
27
  const policy = await client.access.create({
28
28
  subjects: [{ type: UserOntologyType, key: "1" }],
29
29
  objects: [
@@ -40,7 +40,7 @@ describe("Policy", () => {
40
40
  ]);
41
41
  expect(policy.actions).toEqual(["update"]);
42
42
  });
43
- test("create many", async () => {
43
+ test.skip("create many", async () => {
44
44
  const policies = await client.access.create([
45
45
  {
46
46
  subjects: [{ type: UserOntologyType, key: "10" }],
@@ -62,11 +62,11 @@ describe("Policy", () => {
62
62
  actions: ["update"],
63
63
  },
64
64
  ]);
65
- expect(policies.length).toEqual(2);
65
+ expect(policies.length).toEqual(3);
66
66
  expect(policies[0].subjects[0].key).toEqual("10");
67
67
  expect(policies[1].subjects[1].key).toEqual("21");
68
68
  });
69
- test("create instances of policies", async () => {
69
+ test.skip("create instances of policies", async () => {
70
70
  const policy = {
71
71
  key: undefined,
72
72
  subjects: [
@@ -85,7 +85,7 @@ describe("Policy", () => {
85
85
  expect(p.key).not.toEqual(policy.key);
86
86
  });
87
87
  });
88
- test("retrieve by subject", async () => {
88
+ test.skip("retrieve by subject", async () => {
89
89
  const key1 = id.id();
90
90
  const policies = [
91
91
  {
@@ -120,13 +120,13 @@ describe("Policy", () => {
120
120
  expect(p).toHaveLength(2);
121
121
  expect([p[0].actions, p[1].actions].sort()).toEqual([["delete"], ["update"]]);
122
122
  });
123
- test("retrieve by subject - not found", async () => {
123
+ test.skip("retrieve by subject - not found", async () => {
124
124
  const res = await client.access.retrieve({ type: UserOntologyType, key: "999" });
125
125
  expect(res).toHaveLength(0);
126
126
  });
127
127
 
128
128
  describe("delete", async () => {
129
- test("delete one", async () => {
129
+ test.skip("delete one", async () => {
130
130
  const id1 = id.id();
131
131
  const id2 = id.id();
132
132
  const id3 = id.id();
@@ -163,7 +163,7 @@ describe("Policy", () => {
163
163
  expect(res).toHaveLength(1);
164
164
  expect(res[0].actions).toEqual(["delete"]);
165
165
  });
166
- test("delete many", async () => {
166
+ test.skip("delete many", async () => {
167
167
  const id1 = id.id();
168
168
  const id2 = id.id();
169
169
  const id3 = id.id();
@@ -203,7 +203,7 @@ describe("Policy", () => {
203
203
  });
204
204
  });
205
205
  describe("registration", async () => {
206
- test("register a user", async () => {
206
+ test.skip("register a user", async () => {
207
207
  const username = id.id();
208
208
  await client.user.register(username, "pwd1");
209
209
  new Synnax({
@@ -213,14 +213,14 @@ describe("Policy", () => {
213
213
  password: "pwd1",
214
214
  });
215
215
  });
216
- test("duplicate username", async () => {
216
+ test.skip("duplicate username", async () => {
217
217
  const username = id.id();
218
218
  await client.user.register(username, "pwd1");
219
219
  await expect(client.user.register(username, "pwd1")).rejects.toThrow(AuthError);
220
220
  });
221
221
  });
222
222
  describe("privilege", async () => {
223
- test("new user", async () => {
223
+ test.skip("new user", async () => {
224
224
  const username = id.id();
225
225
  const user2 = await client.user.register(username, "pwd1");
226
226
  expect(user2).toBeDefined();
@@ -55,6 +55,7 @@ describe("channelRetriever", () => {
55
55
  rate: Rate.hz(1),
56
56
  leaseholder: 1,
57
57
  index: 0,
58
+ virtual: false,
58
59
  }));
59
60
  });
60
61
  const retriever = new DebouncedBatchRetriever(base, 10);
@@ -81,6 +82,7 @@ describe("channelRetriever", () => {
81
82
  rate: Rate.hz(1),
82
83
  leaseholder: 1,
83
84
  index: 0,
85
+ virtual: false,
84
86
  }));
85
87
  });
86
88
  const retriever = new DebouncedBatchRetriever(base, 10);
@@ -30,6 +30,7 @@ describe("Channel", () => {
30
30
  expect(channel.rate).toEqual(Rate.hz(1));
31
31
  expect(channel.dataType).toEqual(DataType.FLOAT32);
32
32
  });
33
+
33
34
  test("create index and indexed pair", async () => {
34
35
  const one = await client.channels.create({
35
36
  name: "Time",
@@ -44,6 +45,7 @@ describe("Channel", () => {
44
45
  });
45
46
  expect(two.key).not.toEqual(0);
46
47
  });
48
+
47
49
  test("create many", async () => {
48
50
  const channels = await client.channels.create([
49
51
  {
@@ -63,6 +65,7 @@ describe("Channel", () => {
63
65
  expect(channels[0].name).toEqual("test1");
64
66
  expect(channels[1].name).toEqual("test2");
65
67
  });
68
+
66
69
  test("create instances of channels", async () => {
67
70
  const timeIndexChannel = await client.channels.create({
68
71
  name: "time",
@@ -89,6 +92,20 @@ describe("Channel", () => {
89
92
  });
90
93
  await client.channels.create([sensorOne, sensorTwo, sensorThree]);
91
94
  });
95
+
96
+ describe("virtual", () => {
97
+ it("should create a virtual channel", async () => {
98
+ const channel = await client.channels.create({
99
+ name: "test",
100
+ dataType: DataType.JSON,
101
+ virtual: true,
102
+ });
103
+ expect(channel.virtual).toEqual(true);
104
+ const retrieved = await client.channels.retrieve(channel.key);
105
+ expect(retrieved.virtual).toBeTruthy();
106
+ });
107
+ });
108
+
92
109
  describe("retrieveIfNameExists", () => {
93
110
  it("should retrieve the existing channel when it exists", async () => {
94
111
  const name = `test-${Math.random()}-${TimeStamp.now().valueOf()}`;
@@ -159,38 +176,41 @@ describe("Channel", () => {
159
176
  });
160
177
  });
161
178
  });
162
- test("retrieve by key", async () => {
163
- const channel = await client.channels.create({
164
- name: "test",
165
- leaseholder: 1,
166
- rate: Rate.hz(1),
167
- dataType: DataType.FLOAT32,
179
+
180
+ describe("retrieve", () => {
181
+ test("retrieve by key", async () => {
182
+ const channel = await client.channels.create({
183
+ name: "test",
184
+ leaseholder: 1,
185
+ rate: Rate.hz(1),
186
+ dataType: DataType.FLOAT32,
187
+ });
188
+ const retrieved = await client.channels.retrieve(channel.key);
189
+ expect(retrieved.name).toEqual("test");
190
+ expect(retrieved.leaseholder).toEqual(1);
191
+ expect(retrieved.rate).toEqual(Rate.hz(1));
192
+ expect(retrieved.dataType).toEqual(DataType.FLOAT32);
193
+ });
194
+ test("retrieve by key - not found", async () => {
195
+ await expect(
196
+ async () => await client.channels.retrieve("1-1000"),
197
+ ).rejects.toThrow(QueryError);
198
+ });
199
+ test("retrieve by name", async () => {
200
+ const retrieved = await client.channels.retrieve(["test"]);
201
+ expect(retrieved.length).toBeGreaterThan(0);
202
+ retrieved.forEach((ch) => expect(ch.name).toEqual("test"));
203
+ });
204
+ test("retrieve by key - not found", async () => {
205
+ await expect(
206
+ async () => await client.channels.retrieve("1-1000"),
207
+ ).rejects.toThrow(NotFoundError);
208
+ });
209
+ test("retrieve by name", async () => {
210
+ const retrieved = await client.channels.retrieve(["test"]);
211
+ expect(retrieved.length).toBeGreaterThan(0);
212
+ retrieved.forEach((ch) => expect(ch.name).toEqual("test"));
168
213
  });
169
- const retrieved = await client.channels.retrieve(channel.key);
170
- expect(retrieved.name).toEqual("test");
171
- expect(retrieved.leaseholder).toEqual(1);
172
- expect(retrieved.rate).toEqual(Rate.hz(1));
173
- expect(retrieved.dataType).toEqual(DataType.FLOAT32);
174
- });
175
- test("retrieve by key - not found", async () => {
176
- await expect(async () => await client.channels.retrieve("1-1000")).rejects.toThrow(
177
- QueryError,
178
- );
179
- });
180
- test("retrieve by name", async () => {
181
- const retrieved = await client.channels.retrieve(["test"]);
182
- expect(retrieved.length).toBeGreaterThan(0);
183
- retrieved.forEach((ch) => expect(ch.name).toEqual("test"));
184
- });
185
- test("retrieve by key - not found", async () => {
186
- await expect(async () => await client.channels.retrieve("1-1000")).rejects.toThrow(
187
- NotFoundError,
188
- );
189
- });
190
- test("retrieve by name", async () => {
191
- const retrieved = await client.channels.retrieve(["test"]);
192
- expect(retrieved.length).toBeGreaterThan(0);
193
- retrieved.forEach((ch) => expect(ch.name).toEqual("test"));
194
214
  });
195
215
 
196
216
  describe("delete", async () => {
@@ -101,6 +101,11 @@ export class Channel {
101
101
  * should not be relied upon in the current version of Synnax.
102
102
  */
103
103
  readonly alias: string | undefined;
104
+ /**
105
+ * Whether the channel is virtual. Virtual channels do not store any data in the
106
+ * database, but can still be used for streaming purposes.
107
+ */
108
+ readonly virtual: boolean;
104
109
 
105
110
  constructor({
106
111
  dataType,
@@ -111,6 +116,7 @@ export class Channel {
111
116
  isIndex = false,
112
117
  index = 0,
113
118
  internal = false,
119
+ virtual = false,
114
120
  frameClient,
115
121
  alias,
116
122
  }: NewPayload & {
@@ -126,6 +132,7 @@ export class Channel {
126
132
  this.isIndex = isIndex;
127
133
  this.internal = internal;
128
134
  this.alias = alias;
135
+ this.virtual = virtual;
129
136
  this._frameClient = frameClient ?? null;
130
137
  }
131
138
 
@@ -30,6 +30,7 @@ export const payload = z.object({
30
30
  index: z.number(),
31
31
  isIndex: z.boolean(),
32
32
  internal: z.boolean(),
33
+ virtual: z.boolean(),
33
34
  alias: z.string().optional(),
34
35
  });
35
36
 
package/src/client.ts CHANGED
@@ -44,8 +44,8 @@ export const synnaxPropsZ = z.object({
44
44
  required_error: "Port is required",
45
45
  }),
46
46
  ),
47
- username: z.string().optional(),
48
- password: z.string().optional(),
47
+ username: z.string().min(1, "Username is required"),
48
+ password: z.string().min(1, "Password is required"),
49
49
  connectivityPollFrequency: TimeSpan.z.default(TimeSpan.seconds(30)),
50
50
  secure: z.boolean().optional().default(false),
51
51
  name: z.string().optional(),
@@ -79,6 +79,11 @@ export default class Synnax extends framer.Client {
79
79
  static readonly connectivity = connection.Checker;
80
80
  private readonly transport: Transport;
81
81
 
82
+ /**
83
+ * The version of the client.
84
+ */
85
+ readonly clientVersion: string = __VERSION__;
86
+
82
87
  /**
83
88
  * @param props.host - Hostname of a node in the cluster.
84
89
  * @param props.port - Port of the node in the cluster.
@@ -101,10 +106,7 @@ export default class Synnax extends framer.Client {
101
106
  transport.use(errorsMiddleware);
102
107
  let auth_: auth.Client | undefined;
103
108
  if (username != null && password != null) {
104
- auth_ = new auth.Client(transport.unary, {
105
- username,
106
- password,
107
- });
109
+ auth_ = new auth.Client(transport.unary, { username, password });
108
110
  transport.use(auth_.middleware());
109
111
  }
110
112
  const chRetriever = new channel.CacheRetriever(
@@ -120,24 +122,31 @@ export default class Synnax extends framer.Client {
120
122
  this.connectivity = new connection.Checker(
121
123
  transport.unary,
122
124
  connectivityPollFrequency,
125
+ this.clientVersion,
123
126
  props.name,
124
127
  );
125
128
  this.control = new control.Client(this);
126
129
  this.ontology = new ontology.Client(transport.unary, this);
127
130
  const rangeWriter = new ranger.Writer(this.transport.unary);
128
- this.labels = new label.Client(this.transport.unary, this);
131
+ this.labels = new label.Client(this.transport.unary, this, this.ontology);
129
132
  this.ranges = new ranger.Client(
130
133
  this,
131
134
  rangeWriter,
132
135
  this.transport.unary,
133
136
  chRetriever,
134
137
  this.labels,
138
+ this.ontology,
135
139
  );
136
140
  this.access = new access.Client(this.transport.unary);
137
141
  this.user = new user.Client(this.transport.unary);
138
142
  this.workspaces = new workspace.Client(this.transport.unary);
139
143
  const devices = new device.Client(this.transport.unary, this);
140
- const tasks = new task.Client(this.transport.unary, this);
144
+ const tasks = new task.Client(
145
+ this.transport.unary,
146
+ this,
147
+ this.ontology,
148
+ this.ranges,
149
+ );
141
150
  const racks = new rack.Client(this.transport.unary, this, tasks);
142
151
  this.hardware = new hardware.Client(tasks, racks, devices);
143
152
  }
@@ -8,6 +8,7 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  import type { UnaryClient } from "@synnaxlabs/freighter";
11
+ import { migrate } from "@synnaxlabs/x";
11
12
  import { TimeSpan } from "@synnaxlabs/x/telem";
12
13
  import { z } from "zod";
13
14
 
@@ -20,12 +21,16 @@ export const state = z.object({
20
21
  error: z.instanceof(Error).optional(),
21
22
  message: z.string().optional(),
22
23
  clusterKey: z.string(),
24
+ clientVersion: z.string(),
25
+ clientServerCompatible: z.boolean(),
26
+ nodeVersion: z.string().optional(),
23
27
  });
24
28
 
25
29
  export type State = z.infer<typeof state>;
26
30
 
27
31
  const responseZ = z.object({
28
32
  clusterKey: z.string(),
33
+ nodeVersion: z.string().optional(),
29
34
  });
30
35
 
31
36
  const DEFAULT: State = {
@@ -33,6 +38,19 @@ const DEFAULT: State = {
33
38
  status: "disconnected",
34
39
  error: undefined,
35
40
  message: "Disconnected",
41
+ clientServerCompatible: false,
42
+ clientVersion: __VERSION__,
43
+ };
44
+
45
+ const generateWarning = (
46
+ nodeVersion: string | null,
47
+ clientVersion: string,
48
+ clientIsNewer: boolean,
49
+ ): string => {
50
+ const toUpgrade = clientIsNewer ? "cluster" : "client";
51
+ return `Synnax cluster node version ${nodeVersion != null ? nodeVersion + " " : ""}is too ${clientIsNewer ? "old" : "new"} for client version ${clientVersion}.
52
+ This may cause compatibility issues. We recommend updating the ${toUpgrade}. For more information, see
53
+ https://docs.synnaxlabs.com/reference/typescript-client/troubleshooting#old-${toUpgrade}-version`;
36
54
  };
37
55
 
38
56
  /** Polls a synnax cluster for connectivity information. */
@@ -44,8 +62,10 @@ export class Checker {
44
62
  private readonly client: UnaryClient;
45
63
  private readonly name?: string;
46
64
  private interval?: NodeJS.Timeout;
65
+ private readonly clientVersion: string;
47
66
  private readonly onChangeHandlers: Array<(state: State) => void> = [];
48
67
  static readonly connectionStateZ = state;
68
+ private versionWarned = false;
49
69
 
50
70
  /**
51
71
  * @param client - The transport client to use for connectivity checks.
@@ -55,11 +75,13 @@ export class Checker {
55
75
  constructor(
56
76
  client: UnaryClient,
57
77
  pollFreq: TimeSpan = TimeSpan.seconds(30),
78
+ clientVersion: string,
58
79
  name?: string,
59
80
  ) {
60
81
  this._state = { ...DEFAULT };
61
82
  this.client = client;
62
83
  this.pollFrequency = pollFreq;
84
+ this.clientVersion = clientVersion;
63
85
  this.name = name;
64
86
  void this.check();
65
87
  this.startChecking();
@@ -77,11 +99,46 @@ export class Checker {
77
99
  async check(): Promise<State> {
78
100
  const prevStatus = this._state.status;
79
101
  try {
80
- const [res, err] = await this.client.send(Checker.ENDPOINT, {}, z.object({}), responseZ);
102
+ const [res, err] = await this.client.send(
103
+ Checker.ENDPOINT,
104
+ {},
105
+ z.object({}),
106
+ responseZ,
107
+ );
81
108
  if (err != null) throw err;
109
+ const nodeVersion = res.nodeVersion;
110
+ const clientVersion = this.clientVersion;
111
+ const warned = this.versionWarned;
112
+ if (nodeVersion == null) {
113
+ this._state.clientServerCompatible = false;
114
+ if (!warned) {
115
+ console.warn(generateWarning(null, clientVersion, true));
116
+ this.versionWarned = true;
117
+ }
118
+ } else if (
119
+ !migrate.versionsEqual(clientVersion, nodeVersion, {
120
+ checkMajor: true,
121
+ checkMinor: true,
122
+ checkPatch: false,
123
+ })
124
+ ) {
125
+ this._state.clientServerCompatible = false;
126
+ if (!warned) {
127
+ console.warn(
128
+ generateWarning(
129
+ nodeVersion,
130
+ clientVersion,
131
+ migrate.semVerNewer(clientVersion, nodeVersion),
132
+ ),
133
+ );
134
+ this.versionWarned = true;
135
+ }
136
+ } else this._state.clientServerCompatible = true;
82
137
  this._state.status = "connected";
83
138
  this._state.message = `Connected to ${this.name ?? "cluster"}`;
84
139
  this._state.clusterKey = res.clusterKey;
140
+ this._state.nodeVersion = res.nodeVersion;
141
+ this._state.clientVersion = this.clientVersion;
85
142
  } catch (err) {
86
143
  this._state.status = "failed";
87
144
  this._state.error = err as Error;
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { URL } from "@synnaxlabs/x/url";
11
11
  import { describe, expect, it } from "vitest";
12
+ import { z } from "zod";
12
13
 
13
14
  import { auth } from "@/auth";
14
15
  import { Checker } from "@/connection/checker";
@@ -23,8 +24,47 @@ describe("connectivity", () => {
23
24
  password: "seldon",
24
25
  });
25
26
  transport.use(client.middleware());
26
- const connectivity = new Checker(transport.unary);
27
- await connectivity.check();
28
- expect(connectivity.state.status).toEqual("connected");
27
+ const connectivity = new Checker(transport.unary, undefined, __VERSION__);
28
+ const state = await connectivity.check();
29
+ expect(state.status).toEqual("connected");
30
+ expect(z.string().uuid().safeParse(state.clusterKey).success).toBe(true);
31
+ });
32
+ describe("version compatibility", () => {
33
+ it("should pull the server and client versions", async () => {
34
+ const transport = new Transport(new URL({ host: HOST, port: PORT }));
35
+ const client = new auth.Client(transport.unary, {
36
+ username: "synnax",
37
+ password: "seldon",
38
+ });
39
+ transport.use(client.middleware());
40
+ const connectivity = new Checker(transport.unary, undefined, __VERSION__);
41
+ const state = await connectivity.check();
42
+ expect(state.clientServerCompatible).toBe(true);
43
+ expect(state.clientVersion).toBe(__VERSION__);
44
+ });
45
+ it("should adjust state if the server is too old", async () => {
46
+ const transport = new Transport(new URL({ host: HOST, port: PORT }));
47
+ const client = new auth.Client(transport.unary, {
48
+ username: "synnax",
49
+ password: "seldon",
50
+ });
51
+ transport.use(client.middleware());
52
+ const connectivity = new Checker(transport.unary, undefined, "50000.0.0");
53
+ const state = await connectivity.check();
54
+ expect(state.clientServerCompatible).toBe(false);
55
+ expect(state.clientVersion).toBe("50000.0.0");
56
+ });
57
+ it("should adjust state if the server is too new", async () => {
58
+ const transport = new Transport(new URL({ host: HOST, port: PORT }));
59
+ const client = new auth.Client(transport.unary, {
60
+ username: "synnax",
61
+ password: "seldon",
62
+ });
63
+ transport.use(client.middleware());
64
+ const connectivity = new Checker(transport.unary, undefined, "0.0.0");
65
+ const state = await connectivity.check();
66
+ expect(state.clientServerCompatible).toBe(false);
67
+ expect(state.clientVersion).toBe("0.0.0");
68
+ });
29
69
  });
30
70
  });
@@ -1,3 +1,12 @@
1
+ // Copyright 2024 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
1
10
  import { StateTracker } from "@/control/state";
2
11
  import { framer } from "@/framer";
3
12
 
@@ -1,3 +1,12 @@
1
+ // Copyright 2024 Synnax Labs, Inc.
2
+ //
3
+ // Use of this software is governed by the Business Source License included in the file
4
+ // licenses/BSL.txt.
5
+ //
6
+ // As of the Change Date specified in that file, in accordance with the Business Source
7
+ // License, use of this software will be governed by the Apache License, Version 2.0,
8
+ // included in the file licenses/APL.txt.
9
+
1
10
  import { MatchableErrorType } from "@synnaxlabs/freighter/src/errors";
2
11
  import { describe, expect, test } from "vitest";
3
12
 
@@ -15,7 +15,6 @@ import {
15
15
  type MultiSeries,
16
16
  TimeRange,
17
17
  TimeSpan,
18
- toArray,
19
18
  } from "@synnaxlabs/x";
20
19
 
21
20
  import { type Key, type KeyOrName, KeysOrNames, type Params } from "@/channel/payload";
@@ -156,7 +156,7 @@ describe("framer.Frame", () => {
156
156
  });
157
157
 
158
158
  describe("weaklyAligned", () => {
159
- it("should return true if all keys have the same timerange", () => {
159
+ it("should return true if all keys have the same time range", () => {
160
160
  const f = new framer.Frame(
161
161
  new Map([
162
162
  [
@@ -182,7 +182,7 @@ describe("framer.Frame", () => {
182
182
  expect(f.isWeaklyAligned).toEqual(true);
183
183
  });
184
184
 
185
- it("should return false if any key has a different timerange", () => {
185
+ it("should return false if any key has a different time range", () => {
186
186
  const f = new framer.Frame(
187
187
  new Map([
188
188
  [