@synnaxlabs/client 0.26.1 → 0.27.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 (125) hide show
  1. package/.turbo/turbo-build.log +7 -7
  2. package/README.md +36 -10
  3. package/api/client.api.md +3077 -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 +17 -17
  16. package/dist/client.d.ts +6 -7
  17. package/dist/client.d.ts.map +1 -1
  18. package/dist/client.js +2006 -1745
  19. package/dist/connection/checker.d.ts +0 -1
  20. package/dist/control/client.d.ts +0 -1
  21. package/dist/control/client.d.ts.map +1 -1
  22. package/dist/control/state.d.ts +0 -1
  23. package/dist/errors.d.ts +0 -1
  24. package/dist/framer/adapter.d.ts +0 -1
  25. package/dist/framer/client.d.ts +0 -1
  26. package/dist/framer/deleter.d.ts +0 -1
  27. package/dist/framer/frame.d.ts +11 -12
  28. package/dist/framer/iterator.d.ts +0 -1
  29. package/dist/framer/streamProxy.d.ts +0 -1
  30. package/dist/framer/streamer.d.ts +0 -1
  31. package/dist/framer/writer.d.ts +0 -1
  32. package/dist/framer/writer.d.ts.map +1 -1
  33. package/dist/hardware/client.d.ts +0 -1
  34. package/dist/hardware/device/client.d.ts +0 -1
  35. package/dist/hardware/device/payload.d.ts +0 -1
  36. package/dist/hardware/device/payload.d.ts.map +1 -1
  37. package/dist/hardware/rack/client.d.ts +0 -1
  38. package/dist/hardware/rack/payload.d.ts +0 -1
  39. package/dist/hardware/rack/payload.d.ts.map +1 -1
  40. package/dist/hardware/task/client.d.ts +0 -1
  41. package/dist/hardware/task/payload.d.ts +0 -1
  42. package/dist/hardware/task/payload.d.ts.map +1 -1
  43. package/dist/label/client.d.ts +8 -6
  44. package/dist/label/client.d.ts.map +1 -1
  45. package/dist/label/payload.d.ts +0 -1
  46. package/dist/label/retriever.d.ts +0 -1
  47. package/dist/label/retriever.d.ts.map +1 -1
  48. package/dist/label/writer.d.ts +32 -2
  49. package/dist/label/writer.d.ts.map +1 -1
  50. package/dist/ontology/client.d.ts +124 -11
  51. package/dist/ontology/client.d.ts.map +1 -1
  52. package/dist/ontology/group/client.d.ts +0 -1
  53. package/dist/ontology/group/group.d.ts +0 -1
  54. package/dist/ontology/group/payload.d.ts +0 -1
  55. package/dist/ontology/group/writer.d.ts +0 -1
  56. package/dist/ontology/payload.d.ts +5 -1
  57. package/dist/ontology/payload.d.ts.map +1 -1
  58. package/dist/ontology/writer.d.ts +4 -5
  59. package/dist/ontology/writer.d.ts.map +1 -1
  60. package/dist/ranger/alias.d.ts +0 -1
  61. package/dist/ranger/client.d.ts +55 -14
  62. package/dist/ranger/client.d.ts.map +1 -1
  63. package/dist/ranger/external.d.ts +1 -1
  64. package/dist/ranger/external.d.ts.map +1 -1
  65. package/dist/ranger/kv.d.ts +42 -5
  66. package/dist/ranger/kv.d.ts.map +1 -1
  67. package/dist/ranger/payload.d.ts +5 -2
  68. package/dist/ranger/payload.d.ts.map +1 -1
  69. package/dist/ranger/writer.d.ts +107 -2
  70. package/dist/ranger/writer.d.ts.map +1 -1
  71. package/dist/setupspecs.d.ts +0 -1
  72. package/dist/signals/observable.d.ts +0 -1
  73. package/dist/transport.d.ts +0 -1
  74. package/dist/user/client.d.ts +0 -1
  75. package/dist/user/payload.d.ts +0 -1
  76. package/dist/util/retrieve.d.ts +0 -1
  77. package/dist/util/telem.d.ts +0 -1
  78. package/dist/util/zod.d.ts +0 -1
  79. package/dist/workspace/client.d.ts +0 -1
  80. package/dist/workspace/lineplot/client.d.ts +0 -1
  81. package/dist/workspace/lineplot/payload.d.ts +0 -1
  82. package/dist/workspace/lineplot/retriever.d.ts +0 -1
  83. package/dist/workspace/lineplot/writer.d.ts +0 -1
  84. package/dist/workspace/payload.d.ts +0 -1
  85. package/dist/workspace/retriever.d.ts +0 -1
  86. package/dist/workspace/schematic/client.d.ts +0 -1
  87. package/dist/workspace/schematic/payload.d.ts +0 -1
  88. package/dist/workspace/schematic/retriever.d.ts +0 -1
  89. package/dist/workspace/schematic/writer.d.ts +0 -1
  90. package/dist/workspace/writer.d.ts +0 -1
  91. package/package.json +14 -12
  92. package/src/access/access.spec.ts +11 -11
  93. package/src/channel/batchRetriever.spec.ts +2 -0
  94. package/src/channel/channel.spec.ts +51 -31
  95. package/src/channel/client.ts +7 -0
  96. package/src/channel/payload.ts +1 -0
  97. package/src/client.ts +4 -3
  98. package/src/control/client.ts +9 -0
  99. package/src/errors.spec.ts +9 -0
  100. package/src/framer/frame.spec.ts +2 -2
  101. package/src/framer/frame.ts +22 -22
  102. package/src/framer/writer.ts +2 -1
  103. package/src/hardware/device/payload.ts +9 -0
  104. package/src/hardware/rack/payload.ts +9 -0
  105. package/src/hardware/task/payload.ts +9 -0
  106. package/src/label/client.ts +49 -19
  107. package/src/label/label.spec.ts +9 -0
  108. package/src/label/retriever.ts +2 -1
  109. package/src/label/writer.ts +11 -3
  110. package/src/ontology/client.ts +214 -13
  111. package/src/ontology/payload.ts +4 -0
  112. package/src/ontology/writer.ts +26 -12
  113. package/src/ranger/client.ts +204 -28
  114. package/src/ranger/external.ts +1 -1
  115. package/src/ranger/kv.ts +50 -11
  116. package/src/ranger/payload.ts +9 -5
  117. package/src/ranger/ranger.spec.ts +114 -49
  118. package/src/ranger/writer.ts +7 -2
  119. package/vite.config.ts +1 -1
  120. package/dist/ranger/active.d.ts +0 -11
  121. package/dist/ranger/active.d.ts.map +0 -1
  122. package/dist/ranger/range.d.ts +0 -32
  123. package/dist/ranger/range.d.ts.map +0 -1
  124. package/src/ranger/active.ts +0 -74
  125. package/src/ranger/range.ts +0 -98
@@ -8,13 +8,14 @@
8
8
  // included in the file licenses/APL.txt.
9
9
 
10
10
  import { type UnaryClient } from "@synnaxlabs/freighter";
11
+ import { observe } from "@synnaxlabs/x";
11
12
  import { type AsyncTermSearcher } from "@synnaxlabs/x/search";
12
13
 
13
14
  import { type framer } from "@/framer";
14
15
  import { type Key, type Label, labelZ } from "@/label/payload";
15
16
  import { Retriever } from "@/label/retriever";
16
- import { type NewLabelPayload,Writer } from "@/label/writer";
17
- import { type ontology } from "@/ontology";
17
+ import { type NewLabelPayload, SetOptions, Writer } from "@/label/writer";
18
+ import { ontology } from "@/ontology";
18
19
  import { signals } from "@/signals";
19
20
 
20
21
  const LABEL_SET_NAME = "sy_label_set";
@@ -25,11 +26,17 @@ export class Client implements AsyncTermSearcher<string, Key, Label> {
25
26
  private readonly retriever: Retriever;
26
27
  private readonly writer: Writer;
27
28
  private readonly frameClient: framer.Client;
29
+ private readonly ontology: ontology.Client;
28
30
 
29
- constructor(client: UnaryClient, frameClient: framer.Client) {
31
+ constructor(
32
+ client: UnaryClient,
33
+ frameClient: framer.Client,
34
+ ontology: ontology.Client,
35
+ ) {
30
36
  this.writer = new Writer(client);
31
37
  this.retriever = new Retriever(client);
32
38
  this.frameClient = frameClient;
39
+ this.ontology = ontology;
33
40
  }
34
41
 
35
42
  async search(term: string): Promise<Label[]> {
@@ -46,16 +53,20 @@ export class Client implements AsyncTermSearcher<string, Key, Label> {
46
53
  return isMany ? res : res[0];
47
54
  }
48
55
 
49
- async retrieveFor(id: ontology.ID): Promise<Label[]> {
50
- return await this.retriever.retrieveFor(id);
56
+ async retrieveFor(id: ontology.CrudeID): Promise<Label[]> {
57
+ return await this.retriever.retrieveFor(new ontology.ID(id));
51
58
  }
52
59
 
53
- async label(id: ontology.ID, labels: Key[]): Promise<void> {
54
- await this.writer.set(id, labels);
60
+ async label(
61
+ id: ontology.CrudeID,
62
+ labels: Key[],
63
+ opts: SetOptions = {},
64
+ ): Promise<void> {
65
+ await this.writer.set(new ontology.ID(id), labels, opts);
55
66
  }
56
67
 
57
- async removeLabels(id: ontology.ID, labels: Key[]): Promise<void> {
58
- await this.writer.remove(id, labels);
68
+ async removeLabels(id: ontology.CrudeID, labels: Key[]): Promise<void> {
69
+ await this.writer.remove(new ontology.ID(id), labels);
59
70
  }
60
71
 
61
72
  async page(offset: number, limit: number): Promise<Label[]> {
@@ -88,17 +99,36 @@ export class Client implements AsyncTermSearcher<string, Key, Label> {
88
99
  decodeChanges,
89
100
  );
90
101
  }
102
+
103
+ async trackLabelsOf(
104
+ id: ontology.CrudeID,
105
+ ): Promise<observe.ObservableAsyncCloseable<Label[]>> {
106
+ const wrapper = new observe.Observer<Label[]>();
107
+ const initial = (await this.retrieveFor(id)).map((l) => ({
108
+ id: new ontology.ID({ key: l.key, type: "label" }),
109
+ key: l.key,
110
+ name: l.name,
111
+ data: l,
112
+ }));
113
+ const base = await this.ontology.openDependentTracker(
114
+ new ontology.ID(id),
115
+ initial,
116
+ "labeled_by",
117
+ );
118
+ base.onChange((resources: ontology.Resource[]) =>
119
+ wrapper.notify(
120
+ resources.map((r) => ({
121
+ key: r.id.key,
122
+ color: r.data?.color as string,
123
+ name: r.data?.name as string,
124
+ })),
125
+ ),
126
+ );
127
+ return wrapper;
128
+ }
91
129
  }
92
130
 
93
131
  const decodeChanges: signals.Decoder<string, Label> = (variant, data) => {
94
- if (variant === "delete")
95
- return data.toUUIDs().map((v) => ({
96
- variant,
97
- key: v,
98
- }));
99
- return data.parseJSON(labelZ).map((l) => ({
100
- variant,
101
- key: l.key,
102
- value: l,
103
- }));
132
+ if (variant === "delete") return data.toUUIDs().map((v) => ({ variant, key: v }));
133
+ return data.parseJSON(labelZ).map((l) => ({ variant, key: l.key, value: l }));
104
134
  };
@@ -47,5 +47,14 @@ describe("Label", () => {
47
47
  expect(labels).toHaveLength(1);
48
48
  expect(labels[0].key).toEqual(l2.key);
49
49
  });
50
+ it("should replace the labels on an item", async () => {
51
+ const l1 = await client.labels.create({ name: "Label One", color: "#E774D)" });
52
+ const l2 = await client.labels.create({ name: "Label Two", color: "#E774D)" });
53
+ await client.labels.label(label.ontologyID(l1.key), [l2.key]);
54
+ await client.labels.label(label.ontologyID(l1.key), [l1.key], { replace: true });
55
+ const labels = await client.labels.retrieveFor(label.ontologyID(l1.key));
56
+ expect(labels).toHaveLength(1);
57
+ expect(labels[0].key).toEqual(l1.key);
58
+ });
50
59
  });
51
60
  });
@@ -13,6 +13,7 @@ import { z } from "zod";
13
13
 
14
14
  import { keyZ, type Label, labelZ, type Params } from "@/label/payload";
15
15
  import { ontology } from "@/ontology";
16
+ import { nullableArrayZ } from "@/util/zod";
16
17
 
17
18
  const reqZ = z.object({
18
19
  keys: keyZ.array().optional(),
@@ -25,7 +26,7 @@ const reqZ = z.object({
25
26
  type Request = z.infer<typeof reqZ>;
26
27
 
27
28
  const resZ = z.object({
28
- labels: labelZ.array().optional().default([]),
29
+ labels: nullableArrayZ(labelZ),
29
30
  });
30
31
 
31
32
  export class Retriever {
@@ -33,9 +33,13 @@ const deleteReqZ = z.object({
33
33
  const setReqZ = z.object({
34
34
  id: ontology.idZ,
35
35
  labels: keyZ.array(),
36
+ replace: z.boolean().optional(),
36
37
  });
37
38
 
38
- const removeReqZ = setReqZ;
39
+ type SetReq = z.infer<typeof setReqZ>;
40
+ export type SetOptions = Pick<SetReq, "replace">;
41
+
42
+ const removeReqZ = setReqZ.omit({ replace: true });
39
43
 
40
44
  const emptyResZ = z.object({});
41
45
 
@@ -72,11 +76,15 @@ export class Writer {
72
76
  );
73
77
  }
74
78
 
75
- async set(id: ontology.ID, labels: Key[]): Promise<void> {
79
+ async set(
80
+ id: ontology.ID,
81
+ labels: Key[],
82
+ { replace }: SetOptions = {},
83
+ ): Promise<void> {
76
84
  await sendRequired<typeof setReqZ, typeof emptyResZ>(
77
85
  this.client,
78
86
  SET_ENDPOINT,
79
- { id, labels },
87
+ { id, labels, replace },
80
88
  setReqZ,
81
89
  emptyResZ,
82
90
  );
@@ -17,11 +17,13 @@ import { framer } from "@/framer";
17
17
  import { Frame } from "@/framer/frame";
18
18
  import { group } from "@/ontology/group";
19
19
  import {
20
+ CrudeID,
20
21
  ID,
21
22
  IDPayload,
22
23
  idZ,
23
24
  parseRelationship,
24
25
  RelationshipChange,
26
+ RelationshipDirection,
25
27
  type Resource,
26
28
  ResourceChange,
27
29
  resourceSchemaZ,
@@ -52,7 +54,7 @@ const retrieveResZ = z.object({
52
54
  resources: resourceSchemaZ.array(),
53
55
  });
54
56
 
55
- const parseIDs = (ids: ID | ID[] | string | string[]): IDPayload[] =>
57
+ export const parseIDs = (ids: CrudeID | CrudeID[] | string | string[]): IDPayload[] =>
56
58
  toArray(ids).map((id) => new ID(id).payload);
57
59
 
58
60
  /** The core client class for executing queries against a Synnax cluster ontology */
@@ -70,16 +72,50 @@ export class Client implements AsyncTermSearcher<string, string, Resource> {
70
72
  this.framer = framer;
71
73
  }
72
74
 
75
+ /**
76
+ * Executes a fuzzy search on the ontology for resources with names/fields similar to the
77
+ * given term.
78
+ *
79
+ * @param term The search term.
80
+ * @param options Additional options for the search.
81
+ * @param options.includeSchema Whether to include the schema of the resources in the
82
+ * results.
83
+ * @param options.excludeFieldData Whether to exclude the field data of the resources in
84
+ * the results.
85
+ * @returns A list of resources that match the search term.
86
+ */
73
87
  async search(term: string, options?: RetrieveOptions): Promise<Resource[]> {
74
88
  return await this.execRetrieve({ term, ...options });
75
89
  }
76
90
 
77
- async retrieve(id: ID | string, options?: RetrieveOptions): Promise<Resource>;
78
-
79
- async retrieve(ids: ID[] | string[], options?: RetrieveOptions): Promise<Resource[]>;
91
+ /**
92
+ * Retrieves the resource in the ontology with the given ID.
93
+ * @param id The ID of the resource to retrieve.
94
+ * @param options Additional options for the retrieval.
95
+ * @param options.includeSchema Whether to include the schema of the resource in the
96
+ * results.
97
+ * @param options.excludeFieldData Whether to exclude the field data of the resource in
98
+ * the results.
99
+ * @returns The resource with the given ID.
100
+ * @throws {QueryError} If no resource is found with the given ID.
101
+ */
102
+ async retrieve(id: CrudeID, options?: RetrieveOptions): Promise<Resource>;
103
+
104
+ /**
105
+ * Retrieves the resources in the ontology with the given IDs.
106
+ * @param ids The IDs of the resources to retrieve.
107
+ * @param options Additional options for the retrieval.
108
+ * @param options.includeSchema Whether to include the schema of the resources in the
109
+ * results.
110
+ * @param options.excludeFieldData Whether to exclude the field data of the resources in
111
+ * the results.
112
+ * @returns The resources with the given IDs.
113
+ * @throws {QueryError} If no resource is found with any of the given IDs.
114
+ */
115
+ async retrieve(ids: CrudeID[], options?: RetrieveOptions): Promise<Resource[]>;
80
116
 
81
117
  async retrieve(
82
- ids: ID | ID[] | string | string[],
118
+ ids: CrudeID | CrudeID[],
83
119
  options?: RetrieveOptions,
84
120
  ): Promise<Resource | Resource[]> {
85
121
  const resources = await this.execRetrieve({ ids: parseIDs(ids), ...options });
@@ -89,6 +125,15 @@ export class Client implements AsyncTermSearcher<string, string, Resource> {
89
125
  return resources[0];
90
126
  }
91
127
 
128
+ /**
129
+ * Retrieves resources from the ontology in a paginated manner.
130
+ *
131
+ * @param offset - The offset of the page (i.e. how many resources to skip before
132
+ * returning results).
133
+ * @param limit - The maximum number of resources to return.
134
+ * @param options - Additional options for the retrieval.
135
+ * @returns A list of resources in the ontology.
136
+ */
92
137
  async page(
93
138
  offset: number,
94
139
  limit: number,
@@ -97,36 +142,98 @@ export class Client implements AsyncTermSearcher<string, string, Resource> {
97
142
  return await this.execRetrieve({ offset, limit, ...options });
98
143
  }
99
144
 
145
+ /**
146
+ * Retrieves the children of the resources with the given IDs.
147
+ * @param ids - The IDs of the resources whose children to retrieve.
148
+ * @param options - Additional options for the retrieval.
149
+ * @param options.includeSchema - Whether to include the schema of the children in the
150
+ * results.
151
+ * @param options.excludeFieldData - Whether to exclude the field data of the children in
152
+ * the results.
153
+ * @returns The children of the resources with the given IDs.
154
+ */
100
155
  async retrieveChildren(
101
- ids: ID | ID[],
156
+ ids: CrudeID | CrudeID[],
102
157
  options?: RetrieveOptions,
103
158
  ): Promise<Resource[]> {
104
159
  return await this.execRetrieve({ ids: parseIDs(ids), children: true, ...options });
105
160
  }
106
161
 
162
+ /**
163
+ * Retrieves the parents of the resources with the given IDs.
164
+ *
165
+ * @param ids the IDs of the resources whose parents to retrieve
166
+ * @param options additional options for the retrieval
167
+ * @param options.includeSchema whether to include the schema of the parents in the results
168
+ * @param options.excludeFieldData whether to exclude the field data of the parents in the results
169
+ * @returns the parents of the resources with the given IDs
170
+ */
107
171
  async retrieveParents(
108
- ids: ID | ID[],
172
+ ids: CrudeID | CrudeID[],
109
173
  options?: RetrieveOptions,
110
174
  ): Promise<Resource[]> {
111
175
  return await this.execRetrieve({ ids: parseIDs(ids), parents: true, ...options });
112
176
  }
113
177
 
114
- async addChildren(id: ID, ...children: ID[]): Promise<void> {
178
+ /**
179
+ * Adds children to a resource in the ontology.
180
+ * @param id The ID of the resource to add children to.
181
+ * @param children The IDs of the children to add.
182
+ */
183
+ async addChildren(id: CrudeID, ...children: CrudeID[]): Promise<void> {
115
184
  return await this.writer.addChildren(id, ...children);
116
185
  }
117
186
 
118
- async removeChildren(id: ID, ...children: ID[]): Promise<void> {
187
+ /**
188
+ * Removes children from a resource in the ontology.
189
+ * @param id The ID of the resource to remove children from.
190
+ * @param children The IDs of the children
191
+ * to remove.
192
+ */
193
+ async removeChildren(id: CrudeID, ...children: CrudeID[]): Promise<void> {
119
194
  return await this.writer.removeChildren(id, ...children);
120
195
  }
121
196
 
122
- async moveChildren(from: ID, to: ID, ...children: ID[]): Promise<void> {
197
+ /**
198
+ * Moves children from one resource to another in the ontology.
199
+ * @param from The ID of the resource to move children from.
200
+ * @param to The ID of the resource to move children to.
201
+ * @param children The IDs of the children to move.
202
+ */
203
+ async moveChildren(
204
+ from: CrudeID,
205
+ to: CrudeID,
206
+ ...children: CrudeID[]
207
+ ): Promise<void> {
123
208
  return await this.writer.moveChildren(from, to, ...children);
124
209
  }
125
210
 
211
+ /**
212
+ * Opens an observable that can be used to subscribe to changes in both the ontology's
213
+ * resources and relationships.
214
+ * @see ChangeTracker for more information.
215
+ * @returns An observable that emits changes to the ontology's resources and relationships.
216
+ */
126
217
  async openChangeTracker(): Promise<ChangeTracker> {
127
218
  return await ChangeTracker.open(this.framer, this);
128
219
  }
129
220
 
221
+ async openDependentTracker(
222
+ parent: ID,
223
+ initial: Resource[],
224
+ type: string = "parent",
225
+ direction: RelationshipDirection = "from",
226
+ ): Promise<observe.ObservableAsyncCloseable<Resource[]>> {
227
+ return await DependentTracker.open(
228
+ parent,
229
+ this,
230
+ this.framer,
231
+ initial,
232
+ type,
233
+ direction,
234
+ );
235
+ }
236
+
130
237
  newSearcherWithOptions(
131
238
  options: RetrieveOptions,
132
239
  ): AsyncTermSearcher<string, string, Resource> {
@@ -155,13 +262,21 @@ const RESOURCE_DELETE_NAME = "sy_ontology_resource_delete";
155
262
  const RELATIONSHIP_SET_NAME = "sy_ontology_relationship_set";
156
263
  const RELATIONSHIP_DELETE_NAME = "sy_ontology_relationship_delete";
157
264
 
265
+ /**
266
+ * A class that tracks changes to the ontology's resources and relationships.
267
+ */
158
268
  export class ChangeTracker {
159
- private readonly resourceObs: observe.Observer<ResourceChange[]>;
160
- private readonly relationshipObs: observe.Observer<RelationshipChange[]>;
161
-
269
+ /**
270
+ * An observable that emits changes to the ontology's relationships.
271
+ */
162
272
  readonly relationships: observe.Observable<RelationshipChange[]>;
273
+ /**
274
+ * An observable that emits changes to the ontology's resources.
275
+ */
163
276
  readonly resources: observe.Observable<ResourceChange[]>;
164
277
 
278
+ private readonly resourceObs: observe.Observer<ResourceChange[]>;
279
+ private readonly relationshipObs: observe.Observer<RelationshipChange[]>;
165
280
  private readonly streamer: framer.Streamer;
166
281
  private readonly client: Client;
167
282
  private readonly closePromise: Promise<void>;
@@ -256,3 +371,89 @@ export class ChangeTracker {
256
371
  return new ChangeTracker(streamer, retriever);
257
372
  }
258
373
  }
374
+
375
+ /**
376
+ * A class that tracks a resource (called the 'target' resource) and related resources
377
+ * (called 'dependents') of a particular type (called the 'type') in a Synnax cluster
378
+ * ontology.
379
+ */
380
+ export class DependentTracker
381
+ extends observe.Observer<Resource[]>
382
+ implements observe.ObservableAsyncCloseable<Resource[]>
383
+ {
384
+ private readonly internal: ChangeTracker;
385
+ private readonly target: ID;
386
+ private readonly direction: RelationshipDirection;
387
+ private dependents: Resource[];
388
+ private readonly client: Client;
389
+ private readonly type: string;
390
+
391
+ private constructor(
392
+ target: ID,
393
+ internal: ChangeTracker,
394
+ dependents: Resource[],
395
+ client: Client,
396
+ type: string = "parent",
397
+ direction: RelationshipDirection = "from",
398
+ ) {
399
+ super();
400
+ this.internal = internal;
401
+ this.target = target;
402
+ this.dependents = dependents;
403
+ this.client = client;
404
+ this.type = type;
405
+ this.direction = direction;
406
+ this.internal.resources.onChange(this.handleResourceChange);
407
+ this.internal.relationships.onChange(this.handleRelationshipChange);
408
+ }
409
+ static async open(
410
+ from: ID,
411
+ client: Client,
412
+ framer: framer.Client,
413
+ initial: Resource[],
414
+ type: string = "parent",
415
+ direction: RelationshipDirection = "from",
416
+ ): Promise<DependentTracker> {
417
+ const internal = await ChangeTracker.open(framer, client);
418
+ return new DependentTracker(from, internal, initial, client, type, direction);
419
+ }
420
+
421
+ private handleResourceChange = (changes: ResourceChange[]): void => {
422
+ this.dependents = this.dependents.map((child) => {
423
+ const change = changes.find((c) => c.key.toString() == child.id.toString());
424
+ if (change == null || change.variant === "delete") return child;
425
+ return change.value;
426
+ });
427
+ this.notify(this.dependents);
428
+ };
429
+
430
+ private handleRelationshipChange = (changes: RelationshipChange[]): void => {
431
+ const deletes = changes.filter(
432
+ (c) =>
433
+ c.variant === "delete" &&
434
+ c.key[this.direction].toString() === this.target.toString(),
435
+ );
436
+ this.dependents = this.dependents.filter(
437
+ (child) =>
438
+ !deletes.some(
439
+ (del) =>
440
+ del.key.to.toString() === child.id.toString() && del.key.type === this.type,
441
+ ),
442
+ );
443
+ const sets = changes.filter(
444
+ (c) =>
445
+ c.variant === "set" &&
446
+ c.key.type === this.type &&
447
+ c.key[this.direction].toString() === this.target.toString(),
448
+ );
449
+ if (sets.length === 0) return this.notify(this.dependents);
450
+ this.client.retrieve(sets.map((s) => s.key.to)).then((resources) => {
451
+ this.dependents = this.dependents.concat(resources);
452
+ this.notify(this.dependents);
453
+ });
454
+ };
455
+
456
+ async close(): Promise<void> {
457
+ await this.internal.close();
458
+ }
459
+ }
@@ -53,6 +53,8 @@ export const stringIDZ = z.string().transform((v) => {
53
53
 
54
54
  export const crudeIDZ = z.union([stringIDZ, idZ]);
55
55
 
56
+ export type CrudeID = { type: ResourceType; key: string } | string;
57
+
56
58
  export class ID {
57
59
  type: ResourceType;
58
60
  key: string;
@@ -119,6 +121,8 @@ export type Resource<T extends UnknownRecord = UnknownRecord> = Omit<
119
121
  "data"
120
122
  > & { data?: T | null };
121
123
 
124
+ export type RelationshipDirection = "from" | "to";
125
+
122
126
  export const relationshipSchemaZ = z.object({
123
127
  from: ID.z,
124
128
  type: z.string(),
@@ -10,7 +10,7 @@
10
10
  import { sendRequired, type UnaryClient } from "@synnaxlabs/freighter";
11
11
  import { z } from "zod";
12
12
 
13
- import { type ID,idZ } from "@/ontology/payload";
13
+ import { type CrudeID, ID, idZ } from "@/ontology/payload";
14
14
 
15
15
  const ENDPOINTS = {
16
16
  ADD_CHILDREN: "/ontology/add-children",
@@ -36,28 +36,42 @@ export class Writer {
36
36
  this.client = client;
37
37
  }
38
38
 
39
- async addChildren(id: ID, ...children: ID[]): Promise<void> {
39
+ async addChildren(id: CrudeID, ...children: CrudeID[]): Promise<void> {
40
40
  await sendRequired<typeof addRemoveChildrenReqZ, z.ZodTypeAny>(
41
- this.client,
42
- ENDPOINTS.ADD_CHILDREN,
43
- { id, children },
44
- addRemoveChildrenReqZ,
45
- z.object({})
41
+ this.client,
42
+ ENDPOINTS.ADD_CHILDREN,
43
+ { id: new ID(id).payload, children: children.map((c) => new ID(c).payload) },
44
+ addRemoveChildrenReqZ,
45
+ z.object({}),
46
46
  );
47
47
  }
48
48
 
49
- async removeChildren(id: ID, ...children: ID[]): Promise<void> {
49
+ async removeChildren(id: CrudeID, ...children: CrudeID[]): Promise<void> {
50
50
  await sendRequired<typeof addRemoveChildrenReqZ, z.ZodTypeAny>(
51
51
  this.client,
52
52
  ENDPOINTS.REMOVE_CHILDREN,
53
- { id, children },
53
+ { id: new ID(id).payload, children: children.map((c) => new ID(c).payload) },
54
54
  addRemoveChildrenReqZ,
55
55
  z.object({}),
56
56
  );
57
57
  }
58
58
 
59
- async moveChildren(from: ID, to: ID, ...children: ID[]): Promise<void> {
60
- const req = { from, to, children };
61
- await sendRequired(this.client, ENDPOINTS.MOVE_CHILDREN, req, moveChildrenReqZ, z.object({}));
59
+ async moveChildren(
60
+ from: CrudeID,
61
+ to: CrudeID,
62
+ ...children: CrudeID[]
63
+ ): Promise<void> {
64
+ const req = {
65
+ from: new ID(from).payload,
66
+ to: new ID(to).payload,
67
+ children: children.map((c) => new ID(c).payload),
68
+ };
69
+ await sendRequired(
70
+ this.client,
71
+ ENDPOINTS.MOVE_CHILDREN,
72
+ req,
73
+ moveChildrenReqZ,
74
+ z.object({}),
75
+ );
62
76
  }
63
77
  }