@cr_docs_t/dts 0.0.12 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,8 +9,15 @@ export declare class FugueList<P> {
9
9
  totalOrder: UniquelyDenseTotalOrder<P>;
10
10
  positionCounter: number;
11
11
  ws: WebSocket | null;
12
- constructor(totalOrder: UniquelyDenseTotalOrder<P>, ws: WebSocket | null);
12
+ documentID: string;
13
+ readonly batchSize = 100;
14
+ constructor(totalOrder: UniquelyDenseTotalOrder<P>, ws: WebSocket | null, documentID: string);
15
+ /**
16
+ * Propagates message or messages to replicas
17
+ * @param msg - Message or messages to propagate to replicas
18
+ */
13
19
  private propagate;
20
+ private binarySearchPosition;
14
21
  /**
15
22
  * Inserts a value at a given position
16
23
  * @param position - Position to insert at
@@ -29,6 +36,13 @@ export declare class FugueList<P> {
29
36
  * @param value - Value to insert
30
37
  */
31
38
  insert(index: number, value: string): void;
39
+ /**
40
+ * Inserts multiple characters at given index, this
41
+ * handles large insertions by batching messages
42
+ * @param index - Index to insert at
43
+ * @param value - Value to insert
44
+ */
45
+ insertMultiple(index: number, value: string): void;
32
46
  /**
33
47
  * Deletes value at given position
34
48
  * @param position - Position to delete at
@@ -52,12 +66,37 @@ export declare class FugueList<P> {
52
66
  * @param index - Index of the value to delete
53
67
  */
54
68
  delete(index: number): void;
69
+ /**
70
+ * Deletes multiple values starting from index, this
71
+ * handles large deletions by batching messages
72
+ * @param index - Starting index to delete from
73
+ * @param count - Number of values to delete
74
+ */
75
+ deleteMultiple(index: number, count: number): void;
55
76
  /**
56
77
  * Observes the current visible state of the list
57
78
  * @returns The current visible state of the list as a string
58
79
  */
59
80
  observe(): string;
60
- effect(msg: FugueMessage<P>): void;
81
+ /**
82
+ * Applies a single effect message to the list
83
+ * @param msg - Message to apply effect for
84
+ */
85
+ private singleEffect;
86
+ /**
87
+ * Applies batched effect messages to the list
88
+ * @param msgs - Messages to apply effect for in batch
89
+ */
90
+ private batchEffect;
91
+ /**
92
+ * Applies effect messages to the list
93
+ * @param msg - Message or messages to apply effect for, can be batched
94
+ */
95
+ effect(msg: FugueMessage<P> | FugueMessage<P>[]): void;
61
96
  replicaId(): string;
97
+ /**
98
+ * Performs garbage collection by removing tombstoned nodes from the state
99
+ */
100
+ garbageCollect(): void;
62
101
  }
63
102
  //# sourceMappingURL=FugueList.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"FugueList.d.ts","sourceRoot":"","sources":["../../src/Fugue/FugueList.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAC;AACnF,OAAO,EAAE,YAAY,EAAa,MAAM,qBAAqB,CAAC;AAE9D;;GAEG;AACH,qBAAa,SAAS,CAAC,CAAC;IACpB,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAM;IAC1B,UAAU,EAAE,uBAAuB,CAAC,CAAC,CAAC,CAAC;IACvC,eAAe,SAAK;IACpB,EAAE,EAAE,SAAS,GAAG,IAAI,CAAC;gBAET,UAAU,EAAE,uBAAuB,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,SAAS,GAAG,IAAI;IAKxE,OAAO,CAAC,SAAS;IAMjB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAiCxB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IASxB;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAcnC;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAcxB;;;;OAIG;IACH,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAajD;;;;;OAKG;IACH,gBAAgB,CAAC,QAAQ,EAAE,CAAC,GAAG,MAAM,GAAG,SAAS;IAejD;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM;IAoBpB;;;OAGG;IACH,OAAO,IAAI,MAAM;IAmBjB,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC,CAAC;IAc3B,SAAS,IAAI,MAAM;CAGtB"}
1
+ {"version":3,"file":"FugueList.d.ts","sourceRoot":"","sources":["../../src/Fugue/FugueList.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,uBAAuB,EAAE,MAAM,0CAA0C,CAAC;AACnF,OAAO,EAAE,YAAY,EAAa,MAAM,qBAAqB,CAAC;AAE9D;;GAEG;AACH,qBAAa,SAAS,CAAC,CAAC;IACpB,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAM;IAC1B,UAAU,EAAE,uBAAuB,CAAC,CAAC,CAAC,CAAC;IACvC,eAAe,SAAK;IACpB,EAAE,EAAE,SAAS,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,SAAS,OAAO;gBAEb,UAAU,EAAE,uBAAuB,CAAC,CAAC,CAAC,EAAE,EAAE,EAAE,SAAS,GAAG,IAAI,EAAE,UAAU,EAAE,MAAM;IAM5F;;;OAGG;IACH,OAAO,CAAC,SAAS;IAMjB,OAAO,CAAC,oBAAoB;IAmB5B;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IAqBxB;;;;OAIG;IACH,OAAO,CAAC,gBAAgB;IASxB;;;;OAIG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAenC;;;;;OAKG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAgD3C;;;OAGG;IACH,OAAO,CAAC,gBAAgB;IAcxB;;;;OAIG;IACH,mBAAmB,CAAC,KAAK,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAajD;;;;;OAKG;IACH,gBAAgB,CAAC,QAAQ,EAAE,CAAC,GAAG,MAAM,GAAG,SAAS;IAejD;;;OAGG;IACH,MAAM,CAAC,KAAK,EAAE,MAAM;IAqBpB;;;;;OAKG;IACH,cAAc,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM;IAoD3C;;;OAGG;IACH,OAAO,IAAI,MAAM;IAmBjB;;;OAGG;IACH,OAAO,CAAC,YAAY;IAcpB;;;OAGG;IACH,OAAO,CAAC,WAAW;IAqGnB;;;OAGG;IACH,MAAM,CAAC,GAAG,EAAE,YAAY,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,CAAC,EAAE;IAW/C,SAAS,IAAI,MAAM;IAInB;;OAEG;IACH,cAAc;CAOjB"}
@@ -4,49 +4,65 @@ import { Operation } from "../types/Message.js";
4
4
  * A Fugue List CRDT, with insert and delete operations
5
5
  */
6
6
  export class FugueList {
7
- constructor(totalOrder, ws) {
7
+ constructor(totalOrder, ws, documentID) {
8
8
  this.state = [];
9
9
  this.positionCounter = 0;
10
+ this.batchSize = 100;
10
11
  this.totalOrder = totalOrder;
11
12
  this.ws = ws;
13
+ this.documentID = documentID;
12
14
  }
15
+ /**
16
+ * Propagates message or messages to replicas
17
+ * @param msg - Message or messages to propagate to replicas
18
+ */
13
19
  propagate(msg) {
14
20
  if (!this.ws)
15
21
  return;
16
22
  this.ws.send(JSON.stringify(msg));
17
23
  }
24
+ binarySearchPosition(position) {
25
+ let low = 0;
26
+ let high = this.state.length - 1;
27
+ while (low <= high) {
28
+ const mid = Math.floor((low + high) / 2);
29
+ const midPos = this.state[mid][0].position;
30
+ const cmp = this.totalOrder.compare(midPos, position);
31
+ if (cmp === 0) {
32
+ return mid;
33
+ }
34
+ else if (cmp < 0) {
35
+ low = mid + 1;
36
+ }
37
+ else {
38
+ high = mid - 1;
39
+ }
40
+ }
41
+ // If not found, return the position where it can be inserted
42
+ return low;
43
+ }
18
44
  /**
19
45
  * Inserts a value at a given position
20
46
  * @param position - Position to insert at
21
47
  * @param value - Value to insert
22
48
  */
23
49
  insertAtPosition(position, value) {
24
- let index = this.state.length;
25
- for (let i = 0; i < this.state.length; ++i) {
26
- const n = this.state[i][0];
27
- // Compare positions, if the value should be
28
- // ordered before this one, we found our index
29
- // this should fix the issue with inserting in the
30
- // middle messing up order
31
- if (this.totalOrder.compare(position, n.position) < 0) {
32
- index = i;
33
- break;
50
+ let index = this.binarySearchPosition(position);
51
+ // Check if the position already exists at this index, i.e. there is a collision
52
+ if (index < this.state.length && this.totalOrder.compare(this.state[index][0].position, position) === 0) {
53
+ const cell = this.state[index];
54
+ const existing = cell.find((n) => this.totalOrder.compare(n.position, position) === 0);
55
+ // Don't insert if it already exists,
56
+ // TODO: ideally this should trigger a collision resolution
57
+ if (!existing) {
58
+ cell.push(new FNode(position, value));
59
+ cell.sort((a, b) => this.totalOrder.compare(a.position, b.position));
34
60
  }
35
61
  }
36
- console.log({ insertIndex: index });
37
- if (index >= this.state.length) {
38
- this.state.push([]);
39
- }
62
+ // Insert new cell at index
40
63
  else {
41
- this.state.splice(index, 0, []);
64
+ this.state.splice(index, 0, [new FNode(position, value)]);
42
65
  }
43
- // Check if this position already exists at this index
44
- // If it does, we don't insert again
45
- const cell = this.state[index];
46
- const existing = cell.find((n) => this.totalOrder.compare(n.position, position) === 0);
47
- if (existing)
48
- return;
49
- cell.push(new FNode(position, value));
50
66
  }
51
67
  /**
52
68
  * Generates unique position for new element at 'index'
@@ -71,12 +87,59 @@ export class FugueList {
71
87
  console.log({ index, pos });
72
88
  this.insertAtPosition(pos, value);
73
89
  this.propagate({
90
+ documentID: this.documentID,
74
91
  replicaId: this.totalOrder.getReplicaId(),
75
92
  operation: Operation.INSERT,
76
93
  position: pos,
77
94
  data: value,
78
95
  });
79
96
  }
97
+ /**
98
+ * Inserts multiple characters at given index, this
99
+ * handles large insertions by batching messages
100
+ * @param index - Index to insert at
101
+ * @param value - Value to insert
102
+ */
103
+ insertMultiple(index, value) {
104
+ if (value.length == 0)
105
+ return;
106
+ if (value.length == 1) {
107
+ this.insert(index, value);
108
+ return;
109
+ }
110
+ // Find left and right anchors
111
+ const lA = index > 0 ? this.findVisiblePosition(index - 1) : undefined;
112
+ const rA = this.findVisiblePosition(index);
113
+ const newCells = [];
114
+ let cL = lA;
115
+ let msgs = [];
116
+ for (const c of value) {
117
+ const pos = this.totalOrder.createBetween(cL, rA);
118
+ // Collect new cells
119
+ newCells.push([new FNode(pos, c)]);
120
+ // Batch propagate
121
+ msgs.push({
122
+ documentID: this.documentID,
123
+ replicaId: this.totalOrder.getReplicaId(),
124
+ operation: Operation.INSERT,
125
+ position: pos,
126
+ data: c,
127
+ });
128
+ cL = pos;
129
+ if (msgs.length >= this.batchSize) {
130
+ this.propagate(msgs);
131
+ msgs = [];
132
+ }
133
+ }
134
+ // Single splice to insert all new cells
135
+ const firstPos = newCells[0][0].position;
136
+ const insertIndex = this.binarySearchPosition(firstPos);
137
+ this.state.splice(insertIndex, 0, ...newCells);
138
+ // Propagate remaining
139
+ if (msgs.length > 0) {
140
+ this.propagate(msgs);
141
+ }
142
+ }
80
143
  /**
81
144
  * Deletes value at given position
82
145
  * @param position - Position to delete at
@@ -143,12 +206,64 @@ export class FugueList {
143
206
  this.deleteAtPosition(position);
144
207
  // Send to replicas
145
208
  this.propagate({
209
+ documentID: this.documentID,
146
210
  replicaId: this.totalOrder.getReplicaId(),
147
211
  operation: Operation.DELETE,
148
212
  position: position,
149
213
  data: null,
150
214
  });
151
215
  }
216
+ /**
217
+ * Deletes multiple values starting from index, this
218
+ * handles large deletions by batching messages
219
+ * @param index - Starting index to delete from
220
+ * @param count - Number of values to delete
221
+ */
222
+ deleteMultiple(index, count) {
223
+ if (count <= 0)
224
+ return;
225
+ if (count == 1) {
226
+ this.delete(index);
227
+ return;
228
+ }
229
+ let currentVisibleIndex = 0;
230
+ let deletedCount = 0;
231
+ let msgs = [];
232
+ outer: for (const c of this.state) {
233
+ for (const n of c) {
234
+ // Only consider visible nodes
235
+ if (n.value !== undefined) {
236
+ if (currentVisibleIndex >= index) {
237
+ const pos = n.position;
238
+ // Tombstone the node
239
+ n.value = undefined;
240
+ deletedCount++;
241
+ // Batch
242
+ msgs.push({
243
+ documentID: this.documentID,
244
+ replicaId: this.totalOrder.getReplicaId(),
245
+ operation: Operation.DELETE,
246
+ position: pos,
247
+ data: null,
248
+ });
249
+ if (msgs.length >= this.batchSize) {
250
+ this.propagate(msgs);
251
+ msgs = [];
252
+ }
253
+ }
254
+ // Processed a visible node
255
+ currentVisibleIndex++;
256
+ // Check if we've deleted enough
257
+ if (deletedCount >= count) {
258
+ break outer;
259
+ }
260
+ }
261
+ }
262
+ }
263
+ if (msgs.length > 0) {
264
+ this.propagate(msgs);
265
+ }
266
+ }
152
267
  /**
153
268
  * Observes the current visible state of the list
154
269
  * @returns The current visible state of the list as a string
@@ -168,7 +283,11 @@ export class FugueList {
168
283
  }
169
284
  return res.toString();
170
285
  }
171
- effect(msg) {
286
+ /**
287
+ * Applies a single effect message to the list
288
+ * @param msg - Message to apply effect for
289
+ */
290
+ singleEffect(msg) {
172
291
  const { replicaId, operation, data, position } = msg;
173
292
  if (replicaId == this.totalOrder.getReplicaId())
174
293
  return;
@@ -182,7 +301,125 @@ export class FugueList {
182
301
  }
183
302
  throw Error("Invalid operation");
184
303
  }
304
+ /**
305
+ * Applies batched effect messages to the list
306
+ * @param msgs - Messages to apply effect for in batch
307
+ */
308
+ batchEffect(msgs) {
309
+ const inserts = [];
310
+ const deletes = new Set();
311
+ // Separate operations
312
+ for (const msg of msgs) {
313
+ const { replicaId, operation, position, data } = msg;
314
+ if (replicaId == this.totalOrder.getReplicaId())
315
+ continue;
316
+ switch (operation) {
317
+ case Operation.INSERT:
318
+ if (!data)
319
+ continue;
320
+ inserts.push(msg);
321
+ break;
322
+ case Operation.DELETE:
323
+ deletes.add(JSON.stringify(position));
324
+ break;
325
+ }
326
+ }
327
+ // Apply deletes first
328
+ if (deletes.size > 0) {
329
+ for (const posStr of deletes) {
330
+ // Parse the position back from the set
331
+ const pos = JSON.parse(posStr);
332
+ const idx = this.binarySearchPosition(pos);
333
+ // Check if we found the right cell
334
+ if (idx < this.state.length) {
335
+ const cell = this.state[idx];
336
+ // Find the specific node in the collision cell
337
+ const node = cell.find((n) => this.totalOrder.compare(n.position, pos) === 0);
338
+ if (node && node.value !== undefined) {
339
+ node.value = undefined; // Tombstone
340
+ }
341
+ }
342
+ }
343
+ }
344
+ // Then apply inserts
345
+ if (inserts.length > 0) {
346
+ inserts.sort((a, b) => this.totalOrder.compare(a.position, b.position));
347
+ // Group into chunks, of contiguous inserts
348
+ let batchCells = [];
349
+ let startIdx = -1;
350
+ for (const msg of inserts) {
351
+ const { position, data } = msg;
352
+ // Find the index to insert at
353
+ const idx = this.binarySearchPosition(position);
354
+ // Check for collision
355
+ //TODO: should trigger collision resolution
356
+ if (idx < this.state.length && this.totalOrder.compare(this.state[idx][0].position, position) === 0) {
357
+ const cell = this.state[idx];
358
+ const existing = cell.find((n) => this.totalOrder.compare(n.position, position) === 0);
359
+ // Don't insert if it already exists
360
+ if (!existing) {
361
+ cell.push(new FNode(position, data ? data : undefined));
362
+ cell.sort((a, b) => this.totalOrder.compare(a.position, b.position));
363
+ }
364
+ }
365
+ else {
366
+ // Start a new batch if:
367
+ // - It's the first item
368
+ // - This index is not contiguous with the previous group
369
+ if (startIdx === -1) {
370
+ startIdx = idx;
371
+ batchCells = [[new FNode(position, data ? data : undefined)]];
372
+ }
373
+ // If the index is the same as startIdx, continue the batch
374
+ else if (idx === startIdx) {
375
+ batchCells.push([new FNode(position, data ? data : undefined)]);
376
+ }
377
+ // The index is different, i.e. not contiguous, so flush the current batch,
378
+ // commit it and start a new one
379
+ else {
380
+ // Commit batch
381
+ this.state.splice(startIdx, 0, ...batchCells);
382
+ // Start new batch
383
+ // Calculate the shift caused by the splice
384
+ // If the new idx (calculated on old state) is after the splice point,
385
+ // we must add the length of the batch we just inserted.
386
+ const shift = idx >= startIdx ? batchCells.length : 0;
387
+ startIdx = idx + shift;
388
+ batchCells = [[new FNode(position, data ? data : undefined)]];
389
+ }
390
+ }
391
+ }
392
+ // Commit any remaining batch
393
+ if (batchCells.length > 0)
394
+ this.state.splice(startIdx, 0, ...batchCells);
395
+ }
396
+ }
397
+ /**
398
+ * Applies effect messages to the list
399
+ * @param msg - Message or messages to apply effect for, can be batched
400
+ */
401
+ effect(msg) {
402
+ if (Array.isArray(msg)) {
403
+ this.batchEffect(msg);
404
+ // for (const m of msg) {
405
+ // this.singleEffect(m);
406
+ // }
407
+ }
408
+ else {
409
+ this.singleEffect(msg);
410
+ }
411
+ }
185
412
  replicaId() {
186
413
  return this.totalOrder.getReplicaId();
187
414
  }
415
+ /**
416
+ * Performs garbage collection by removing tombstoned nodes from the state
417
+ */
418
+ garbageCollect() {
419
+ this.state = this.state.filter((cell) => {
420
+ // Check if cell has any visible nodes
421
+ const hasVisible = cell.some((n) => n.value !== undefined);
422
+ return hasVisible;
423
+ });
424
+ }
188
425
  }
@@ -6,6 +6,7 @@ export declare enum Operation {
6
6
  }
7
7
  export type Data = string;
8
8
  export interface FugueMessage<P> {
9
+ documentID: string;
9
10
  replicaId: string;
10
11
  operation: Operation;
11
12
  position: P;
@@ -1 +1 @@
1
- {"version":3,"file":"Message.d.ts","sourceRoot":"","sources":["../../src/types/Message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,oBAAY,SAAS;IACjB,MAAM,IAAA;IACN,MAAM,IAAA;IACN,IAAI,IAAA;CACP;AAED,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC;AAE1B,MAAM,WAAW,YAAY,CAAC,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,CAAC,CAAC;IACZ,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB,CAAC,CAAC;IAC/B,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;CACxB"}
1
+ {"version":3,"file":"Message.d.ts","sourceRoot":"","sources":["../../src/types/Message.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,YAAY,CAAC;AAExC,oBAAY,SAAS;IACjB,MAAM,IAAA;IACN,MAAM,IAAA;IACN,IAAI,IAAA;CACP;AAED,MAAM,MAAM,IAAI,GAAG,MAAM,CAAC;AAE1B,MAAM,WAAW,YAAY,CAAC,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,SAAS,CAAC;IACrB,QAAQ,EAAE,CAAC,CAAC;IACZ,IAAI,EAAE,IAAI,GAAG,IAAI,CAAC;CACrB;AAED,MAAM,WAAW,gBAAgB,CAAC,CAAC;IAC/B,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,CAAC;CACxB"}
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "@cr_docs_t/dts",
3
- "version": "0.0.12",
3
+ "version": "0.2.1",
4
4
  "description": "",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
7
7
  "scripts": {
8
8
  "build": "tsc",
9
- "prepublishOnly": "npm run build"
9
+ "prepublishOnly": "npm run build",
10
+ "ci": "npm install && npm run build",
11
+ "publish": "npm publish --access public"
10
12
  },
11
13
  "exports": {
12
14
  ".": {
@@ -15,6 +17,35 @@
15
17
  "types": "./dist/index.d.ts"
16
18
  }
17
19
  },
20
+ "publishConfig": {
21
+ "access": "public"
22
+ },
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/CRDT-Capstone/CRDocsT-dts.git"
26
+ },
27
+ "release": {
28
+ "branches": [
29
+ "master"
30
+ ],
31
+ "plugins": [
32
+ "@semantic-release/commit-analyzer",
33
+ "@semantic-release/release-notes-generator",
34
+ "@semantic-release/changelog",
35
+ "@semantic-release/npm",
36
+ "@semantic-release/github",
37
+ [
38
+ "@semantic-release/git",
39
+ {
40
+ "assets": [
41
+ "package.json",
42
+ "CHANGELOG.md"
43
+ ],
44
+ "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
45
+ }
46
+ ]
47
+ ]
48
+ },
18
49
  "keywords": [],
19
50
  "author": [
20
51
  "Madiba Hudson-Quansah",
@@ -23,7 +54,10 @@
23
54
  "license": "ISC",
24
55
  "packageManager": "pnpm@10.20.0",
25
56
  "devDependencies": {
57
+ "@semantic-release/changelog": "^6.0.3",
58
+ "@semantic-release/git": "^10.0.1",
26
59
  "@types/node": "^24.10.1",
60
+ "semantic-release": "^25.0.2",
27
61
  "typescript": "^5.9.3"
28
62
  },
29
63
  "files": [