@orkestrel/workspace 0.0.6 → 0.0.7

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.
@@ -3,7 +3,7 @@ import { createDatabase, createMemoryDriver } from "@orkestrel/database";
3
3
  import { Emitter } from "@orkestrel/emitter";
4
4
  //#region src/core/constants.ts
5
5
  /**
6
- * File extensions mapped to language tags for text content.
6
+ * Maps file extensions to language tags for text content.
7
7
  *
8
8
  * Unknown extensions intentionally fall back to `text` in
9
9
  * {@link import('./helpers.js').inferLanguage}.
@@ -52,12 +52,12 @@ var EXTENSION_LANGUAGES = Object.freeze({
52
52
  });
53
53
  //#endregion
54
54
  //#region src/core/errors.ts
55
- /** An invalid workspace edit or search operation. */
55
+ /** Reports an invalid workspace edit or search operation. */
56
56
  var WorkspaceError = class extends Error {
57
57
  code;
58
58
  context;
59
59
  /**
60
- * Create a workspace error.
60
+ * Creates a workspace error.
61
61
  *
62
62
  * @param code - The machine-readable failure code
63
63
  * @param message - The human-readable failure message
@@ -71,10 +71,10 @@ var WorkspaceError = class extends Error {
71
71
  }
72
72
  };
73
73
  /**
74
- * Narrow a caught value to a {@link WorkspaceError}.
74
+ * Narrows a caught value to a {@link WorkspaceError}.
75
75
  *
76
76
  * @param value - The caught value
77
- * @returns Whether the value is a workspace error
77
+ * @returns True if the value is a workspace error; false otherwise
78
78
  *
79
79
  * @example
80
80
  * ```ts
@@ -87,7 +87,7 @@ function isWorkspaceError(value) {
87
87
  //#endregion
88
88
  //#region src/core/helpers.ts
89
89
  /**
90
- * Infer a language tag from the final file extension.
90
+ * Infers a language tag from the final file extension.
91
91
  *
92
92
  * @param path - The file path
93
93
  * @returns The mapped language, or `text` when the extension is absent or unknown
@@ -103,10 +103,10 @@ function inferLanguage(path) {
103
103
  return EXTENSION_LANGUAGES[path.slice(dot + 1).toLowerCase()] ?? "text";
104
104
  }
105
105
  /**
106
- * Determine whether content is the text arm.
106
+ * Determines whether content is the text arm.
107
107
  *
108
108
  * @param content - The file content
109
- * @returns Whether the content carries text and a language
109
+ * @returns True if the content carries text and a language; false otherwise
110
110
  *
111
111
  * @example
112
112
  * ```ts
@@ -117,57 +117,21 @@ function isText(content) {
117
117
  return "text" in content;
118
118
  }
119
119
  /**
120
- * Determine whether content is the binary arm.
120
+ * Checks whether content is the binary arm.
121
121
  *
122
122
  * @param content - The file content
123
- * @returns Whether the content carries base64 data and a MIME
123
+ * @returns True if the content carries a base64 string and a MIME; false otherwise
124
124
  *
125
125
  * @example
126
126
  * ```ts
127
- * isBinary({ data: 'AAAA', mime: 'image/png' }) // true
127
+ * isBinary({ base64: 'AAAA', mime: 'image/png' }) // true
128
128
  * ```
129
129
  */
130
130
  function isBinary(content) {
131
- return "data" in content;
131
+ return "base64" in content;
132
132
  }
133
133
  /**
134
- * Narrow an unknown value to an immutable file record.
135
- *
136
- * @param value - The value to inspect
137
- * @returns Whether the value has the file shape
138
- *
139
- * @example
140
- * ```ts
141
- * isFile({ path: 'a.txt', content: { text: 'a', language: 'text' }, state: 'created', size: 1, lines: 1 })
142
- * ```
143
- */
144
- function isFile(value) {
145
- return holds(() => {
146
- if (!isRecord(value)) return false;
147
- if (!isString(value.path) || !literalOf("created", "modified")(value.state)) return false;
148
- if (!isNumber(value.size) || !isNumber(value.lines)) return false;
149
- if (!isRecord(value.content)) return false;
150
- const text = isString(value.content.text) && isString(value.content.language);
151
- const binary = isString(value.content.data) && literalOf("image/png", "image/jpeg", "image/gif", "image/webp")(value.content.mime);
152
- return text || binary;
153
- });
154
- }
155
- /**
156
- * Narrow an unknown value to a workspace snapshot.
157
- *
158
- * @param value - The value read from a persistence boundary
159
- * @returns Whether the value has the workspace snapshot shape
160
- *
161
- * @example
162
- * ```ts
163
- * isWorkspaceSnapshot({ id: 'work', files: [] }) // true
164
- * ```
165
- */
166
- function isWorkspaceSnapshot(value) {
167
- return holds(() => isRecord(value) && isString(value.id) && arrayOf(isFile)(value.files));
168
- }
169
- /**
170
- * Compute the byte size of file content.
134
+ * Computes the byte size of file content.
171
135
  *
172
136
  * @param content - The content to measure
173
137
  * @returns UTF-8 bytes for text or decoded bytes for binary content
@@ -179,10 +143,10 @@ function isWorkspaceSnapshot(value) {
179
143
  */
180
144
  function computeSize(content) {
181
145
  if (isText(content)) return new TextEncoder().encode(content.text).length;
182
- return decodedSize(content.data);
146
+ return computeDecodedSize(content.base64);
183
147
  }
184
148
  /**
185
- * Count the lines in file content.
149
+ * Counts the lines in file content.
186
150
  *
187
151
  * @param content - The content to inspect
188
152
  * @returns The text line count, or zero for empty text and binary content
@@ -200,27 +164,27 @@ function countLines(content) {
200
164
  return count;
201
165
  }
202
166
  /**
203
- * Compute the decoded byte length of a base64 string.
167
+ * Computes the decoded byte length of a base64 string.
204
168
  *
205
- * @param base64 - The base64 data
169
+ * @param base64 - The base64 string
206
170
  * @returns The decoded byte count
207
171
  *
208
172
  * @example
209
173
  * ```ts
210
- * decodedSize('AAAA') // 3
174
+ * computeDecodedSize('AAAA') // 3
211
175
  * ```
212
176
  */
213
- function decodedSize(base64) {
177
+ function computeDecodedSize(base64) {
214
178
  let padding = 0;
215
179
  if (base64.endsWith("==")) padding = 2;
216
180
  else if (base64.endsWith("=")) padding = 1;
217
181
  return Math.floor(base64.length * 3 / 4) - padding;
218
182
  }
219
183
  /**
220
- * Determine whether a 1-based range is structurally valid.
184
+ * Determines whether a 1-based range is structurally valid.
221
185
  *
222
186
  * @param range - The range to inspect
223
- * @returns Whether both positions are positive and ordered
187
+ * @returns True if both positions are positive and ordered; false otherwise
224
188
  *
225
189
  * @example
226
190
  * ```ts
@@ -234,7 +198,7 @@ function isValidRange(range) {
234
198
  return !(range.start.line === range.end.line && range.start.column > range.end.column);
235
199
  }
236
200
  /**
237
- * Clamp a position to text bounds.
201
+ * Clamps a position to text bounds.
238
202
  *
239
203
  * @param text - The addressed text
240
204
  * @param position - The requested position
@@ -255,7 +219,7 @@ function clampPosition(text, position) {
255
219
  };
256
220
  }
257
221
  /**
258
- * Clamp both positions in a range to text bounds.
222
+ * Clamps both positions in a range to text bounds.
259
223
  *
260
224
  * @param text - The addressed text
261
225
  * @param range - The requested range
@@ -273,7 +237,7 @@ function clampRange(text, range) {
273
237
  };
274
238
  }
275
239
  /**
276
- * Convert a 1-based position to a zero-based string offset.
240
+ * Converts a 1-based position to a zero-based string offset.
277
241
  *
278
242
  * @param text - The addressed text
279
243
  * @param position - The requested position
@@ -292,7 +256,7 @@ function offsetAt(text, position) {
292
256
  return Math.min(offset, text.length);
293
257
  }
294
258
  /**
295
- * Slice a clamped half-open text range.
259
+ * Slices a clamped half-open text range.
296
260
  *
297
261
  * @param text - The source text
298
262
  * @param range - The requested range
@@ -308,7 +272,7 @@ function sliceRange(text, range) {
308
272
  return text.slice(offsetAt(text, clamped.start), offsetAt(text, clamped.end));
309
273
  }
310
274
  /**
311
- * Replace a clamped half-open text range.
275
+ * Replaces a clamped half-open text range.
312
276
  *
313
277
  * @param text - The source text
314
278
  * @param range - The requested range
@@ -327,7 +291,7 @@ function spliceRange(text, range, replacement) {
327
291
  return text.slice(0, start) + replacement + text.slice(end);
328
292
  }
329
293
  /**
330
- * Assemble a nested range from four 1-based coordinates.
294
+ * Assembles a nested range from four 1-based coordinates.
331
295
  *
332
296
  * @param fromLine - The start line
333
297
  * @param fromColumn - The start column
@@ -353,7 +317,7 @@ function rangeOf(fromLine, fromColumn, toLine, toColumn) {
353
317
  };
354
318
  }
355
319
  /**
356
- * Escape regular-expression metacharacters for literal matching.
320
+ * Escapes regular-expression metacharacters for literal matching.
357
321
  *
358
322
  * @param value - The literal text
359
323
  * @returns The escaped regular-expression source
@@ -367,9 +331,47 @@ function escapeRegExp(value) {
367
331
  return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
368
332
  }
369
333
  //#endregion
334
+ //#region src/core/validators.ts
335
+ /**
336
+ * Narrows an unknown value to an immutable file record.
337
+ *
338
+ * @param value - The value to inspect
339
+ * @returns True if the value has the file shape; false otherwise
340
+ *
341
+ * @example
342
+ * ```ts
343
+ * isFile({ path: 'a.txt', content: { text: 'a', language: 'text' }, state: 'created', size: 1, lines: 1 })
344
+ * ```
345
+ */
346
+ function isFile(value) {
347
+ return holds(() => {
348
+ if (!isRecord(value)) return false;
349
+ if (!isString(value.path) || !literalOf("created", "modified")(value.state)) return false;
350
+ if (!isNumber(value.size) || !isNumber(value.lines)) return false;
351
+ if (!isRecord(value.content)) return false;
352
+ const text = isString(value.content.text) && isString(value.content.language);
353
+ const binary = isString(value.content.base64) && literalOf("image/png", "image/jpeg", "image/gif", "image/webp")(value.content.mime);
354
+ return text || binary;
355
+ });
356
+ }
357
+ /**
358
+ * Narrows an unknown value to a workspace snapshot.
359
+ *
360
+ * @param value - The value read from a persistence boundary
361
+ * @returns True if the value has the workspace snapshot shape; false otherwise
362
+ *
363
+ * @example
364
+ * ```ts
365
+ * isWorkspaceSnapshot({ id: 'work', files: [] }) // true
366
+ * ```
367
+ */
368
+ function isWorkspaceSnapshot(value) {
369
+ return holds(() => isRecord(value) && isString(value.id) && arrayOf(isFile)(value.files));
370
+ }
371
+ //#endregion
370
372
  //#region src/core/workspaces/Workspace.ts
371
373
  /**
372
- * A mutable path-keyed editing surface over immutable files.
374
+ * Provides a mutable path-keyed editing surface over immutable files.
373
375
  *
374
376
  * Whole-file edits create text files, ranged edits operate only on existing text files, and
375
377
  * binary files remain available through construction-time hydration. Mutations emit after the
@@ -390,7 +392,7 @@ var Workspace = class {
390
392
  #files = /* @__PURE__ */ new Map();
391
393
  #emitter;
392
394
  /**
393
- * Create a workspace.
395
+ * Creates a workspace.
394
396
  *
395
397
  * @param options - Optional identity, emitter configuration, and initial files
396
398
  */
@@ -436,7 +438,7 @@ var Workspace = class {
436
438
  };
437
439
  }
438
440
  has(path) {
439
- if (isArray(path)) return path.some((one) => this.#files.has(one));
441
+ if (isArray(path)) return path.every((one) => this.#files.has(one));
440
442
  return this.#files.has(path);
441
443
  }
442
444
  search(query, options) {
@@ -446,7 +448,7 @@ var Workspace = class {
446
448
  for (const file of this.#files.values()) {
447
449
  if (limit !== void 0 && matches.length >= limit) break;
448
450
  if (!isText(file.content)) continue;
449
- const lines = file.content.text.split("\n");
451
+ const lines = file.content.text.split(/\r\n|\n/);
450
452
  for (let index = 0; index < lines.length; index += 1) {
451
453
  if (limit !== void 0 && matches.length >= limit) break;
452
454
  const lineText = lines[index] ?? "";
@@ -500,39 +502,42 @@ var Workspace = class {
500
502
  for (const [one, text] of Object.entries(path)) this.#write(one, text);
501
503
  return;
502
504
  }
503
- const text = content ?? "";
505
+ if (content === void 0) return;
504
506
  if (!range) {
505
- this.#write(path, text);
507
+ this.#write(path, content);
506
508
  return;
507
509
  }
508
- this.#splice(path, text, range);
510
+ this.#splice(path, content, range);
509
511
  }
510
512
  prepend(path, content) {
511
513
  if (isRecord(path)) {
512
514
  for (const [one, text] of Object.entries(path)) this.#prepend(one, text);
513
515
  return;
514
516
  }
515
- this.#prepend(path, content ?? "");
517
+ if (content === void 0) return;
518
+ this.#prepend(path, content);
516
519
  }
517
520
  append(path, content) {
518
521
  if (isRecord(path)) {
519
522
  for (const [one, text] of Object.entries(path)) this.#append(one, text);
520
523
  return;
521
524
  }
522
- this.#append(path, content ?? "");
525
+ if (content === void 0) return;
526
+ this.#append(path, content);
523
527
  }
524
528
  move(from, to) {
525
529
  if (isRecord(from)) {
526
- let moved = false;
527
- for (const [one, target] of Object.entries(from)) if (this.#move(one, target)) moved = true;
530
+ let moved = true;
531
+ for (const [one, target] of Object.entries(from)) if (!this.#move(one, target)) moved = false;
528
532
  return moved;
529
533
  }
530
- return this.#move(from, to ?? "");
534
+ if (to === void 0) return false;
535
+ return this.#move(from, to);
531
536
  }
532
537
  remove(path) {
533
538
  if (isArray(path)) {
534
- let removed = false;
535
- for (const one of path) if (this.#remove(one)) removed = true;
539
+ let removed = true;
540
+ for (const one of path) if (!this.#remove(one)) removed = false;
536
541
  return removed;
537
542
  }
538
543
  return this.#remove(path);
@@ -565,7 +570,7 @@ var Workspace = class {
565
570
  }
566
571
  #splice(path, content, range) {
567
572
  const existing = this.#files.get(path);
568
- if (!existing) throw new WorkspaceError("MODALITY", `Cannot splice a range of a missing file: ${path}`, { path });
573
+ if (!existing) throw new WorkspaceError("MISSING", `Cannot splice a range of a missing file: ${path}`, { path });
569
574
  if (!isText(existing.content)) throw new WorkspaceError("MODALITY", `Cannot splice a range of a binary file: ${path}`, { path });
570
575
  if (!isValidRange(range)) throw new WorkspaceError("RANGE", `Invalid range for file: ${path}`, {
571
576
  path,
@@ -634,7 +639,7 @@ var Workspace = class {
634
639
  //#endregion
635
640
  //#region src/core/workspaces/WorkspaceManager.ts
636
641
  /**
637
- * An insertion-ordered workspace registry with an active selection.
642
+ * Provides an insertion-ordered workspace registry with an active selection.
638
643
  *
639
644
  * A supplied store adds lenient snapshot `open` and `save` operations. Event defaults flow into
640
645
  * workspaces created through the registry, while observability remains owned by each workspace.
@@ -655,7 +660,7 @@ var WorkspaceManager = class {
655
660
  #error;
656
661
  #store;
657
662
  /**
658
- * Create a registry.
663
+ * Creates a registry.
659
664
  *
660
665
  * @param options - Default event hooks and optional snapshot store
661
666
  */
@@ -719,8 +724,8 @@ var WorkspaceManager = class {
719
724
  }
720
725
  remove(ids) {
721
726
  if (isArray(ids)) {
722
- let removed = false;
723
- for (const id of ids) if (this.#drop(id)) removed = true;
727
+ let removed = true;
728
+ for (const id of ids) if (!this.#drop(id)) removed = false;
724
729
  return removed;
725
730
  }
726
731
  return this.#drop(ids);
@@ -742,21 +747,27 @@ var WorkspaceManager = class {
742
747
  //#endregion
743
748
  //#region src/core/workspaces/stores/DatabaseWorkspaceStore.ts
744
749
  /**
745
- * A workspace snapshot store over a database table.
750
+ * Persists workspace snapshots in a database table.
746
751
  *
747
752
  * Snapshots occupy one opaque column and are narrowed when read back from the storage boundary.
748
753
  *
749
754
  * @example
750
755
  * ```ts
751
- * import { createDatabaseWorkspaceStore } from '@orkestrel/workspace'
752
- *
753
- * const store = createDatabaseWorkspaceStore()
756
+ * import { rawShape, stringShape } from '@orkestrel/contract'
757
+ * import { createDatabase, createMemoryDriver } from '@orkestrel/database'
758
+ * import { DatabaseWorkspaceStore } from '@orkestrel/workspace'
759
+ *
760
+ * const database = createDatabase({
761
+ * driver: createMemoryDriver(),
762
+ * tables: { workspaces: { id: stringShape(), snapshot: rawShape({}) } },
763
+ * })
764
+ * const store = new DatabaseWorkspaceStore(database.table('workspaces'))
754
765
  * ```
755
766
  */
756
767
  var DatabaseWorkspaceStore = class {
757
768
  #table;
758
769
  /**
759
- * Wrap a workspace snapshot table.
770
+ * Wraps a workspace snapshot table.
760
771
  *
761
772
  * @param table - The table keyed by workspace identifier
762
773
  */
@@ -781,7 +792,7 @@ var DatabaseWorkspaceStore = class {
781
792
  //#endregion
782
793
  //#region src/core/workspaces/stores/MemoryWorkspaceStore.ts
783
794
  /**
784
- * A process-local workspace snapshot store.
795
+ * Holds workspace snapshots in the current process.
785
796
  *
786
797
  * @example
787
798
  * ```ts
@@ -807,7 +818,7 @@ var MemoryWorkspaceStore = class {
807
818
  //#endregion
808
819
  //#region src/core/factories.ts
809
820
  /**
810
- * Create an immutable file with derived size and line counts.
821
+ * Creates an immutable file with derived size and line counts.
811
822
  *
812
823
  * @param input - The file path, content, and optional state
813
824
  * @returns A frozen file record
@@ -829,7 +840,7 @@ function createFile(input) {
829
840
  });
830
841
  }
831
842
  /**
832
- * Create the text arm of {@link FileContent}.
843
+ * Creates the text arm of {@link FileContent}.
833
844
  *
834
845
  * @param text - The text body
835
846
  * @param language - The language tag
@@ -849,9 +860,9 @@ function createTextContent(text, language) {
849
860
  };
850
861
  }
851
862
  /**
852
- * Create the binary arm of {@link FileContent}.
863
+ * Creates the binary arm of {@link FileContent}.
853
864
  *
854
- * @param data - The base64 payload
865
+ * @param base64 - The base64 payload
855
866
  * @param mime - The binary MIME
856
867
  * @returns Binary file content
857
868
  *
@@ -862,14 +873,14 @@ function createTextContent(text, language) {
862
873
  * createBinaryContent('AAAA', 'image/png')
863
874
  * ```
864
875
  */
865
- function createBinaryContent(data, mime) {
876
+ function createBinaryContent(base64, mime) {
866
877
  return {
867
- data,
878
+ base64,
868
879
  mime
869
880
  };
870
881
  }
871
882
  /**
872
- * Create a workspace.
883
+ * Creates a workspace.
873
884
  *
874
885
  * @param options - Optional identity, emitter configuration, and initial files
875
886
  * @returns A working workspace
@@ -886,7 +897,7 @@ function createWorkspace(options) {
886
897
  return new Workspace(options);
887
898
  }
888
899
  /**
889
- * Create an in-memory workspace snapshot store.
900
+ * Creates an in-memory workspace snapshot store.
890
901
  *
891
902
  * @returns A process-local workspace store
892
903
  *
@@ -901,9 +912,9 @@ function createMemoryWorkspaceStore() {
901
912
  return new MemoryWorkspaceStore();
902
913
  }
903
914
  /**
904
- * Create a database-backed workspace snapshot store.
915
+ * Creates a database-backed workspace snapshot store.
905
916
  *
906
- * @param driver - The database driver; defaults to an in-memory driver
917
+ * @param driver - The database driver. Default: an in-memory driver.
907
918
  * @returns A workspace store backed by the supplied driver
908
919
  *
909
920
  * @example
@@ -924,7 +935,7 @@ function createDatabaseWorkspaceStore(driver = createMemoryDriver()) {
924
935
  }).table("workspaces"));
925
936
  }
926
937
  /**
927
- * Create an empty workspace registry.
938
+ * Creates an empty workspace registry.
928
939
  *
929
940
  * @param options - Default event hooks and optional durability
930
941
  * @returns A workspace manager
@@ -941,6 +952,6 @@ function createWorkspaceManager(options) {
941
952
  return new WorkspaceManager(options);
942
953
  }
943
954
  //#endregion
944
- export { DatabaseWorkspaceStore, EXTENSION_LANGUAGES, MemoryWorkspaceStore, Workspace, WorkspaceError, WorkspaceManager, clampPosition, clampRange, computeSize, countLines, createBinaryContent, createDatabaseWorkspaceStore, createFile, createMemoryWorkspaceStore, createTextContent, createWorkspace, createWorkspaceManager, decodedSize, escapeRegExp, inferLanguage, isBinary, isFile, isText, isValidRange, isWorkspaceError, isWorkspaceSnapshot, offsetAt, rangeOf, sliceRange, spliceRange };
955
+ export { DatabaseWorkspaceStore, EXTENSION_LANGUAGES, MemoryWorkspaceStore, Workspace, WorkspaceError, WorkspaceManager, clampPosition, clampRange, computeDecodedSize, computeSize, countLines, createBinaryContent, createDatabaseWorkspaceStore, createFile, createMemoryWorkspaceStore, createTextContent, createWorkspace, createWorkspaceManager, escapeRegExp, inferLanguage, isBinary, isFile, isText, isValidRange, isWorkspaceError, isWorkspaceSnapshot, offsetAt, rangeOf, sliceRange, spliceRange };
945
956
 
946
957
  //# sourceMappingURL=index.js.map