@orkestrel/workspace 0.0.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.
@@ -0,0 +1,976 @@
1
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
2
+ let _orkestrel_contract = require("@orkestrel/contract");
3
+ let _orkestrel_database = require("@orkestrel/database");
4
+ let _orkestrel_emitter = require("@orkestrel/emitter");
5
+ //#region src/core/constants.ts
6
+ /**
7
+ * File extensions mapped to language tags for text content.
8
+ *
9
+ * Unknown extensions intentionally fall back to `text` in
10
+ * {@link import('./helpers.js').inferLanguage}.
11
+ */
12
+ var EXTENSION_LANGUAGES = Object.freeze({
13
+ ts: "typescript",
14
+ tsx: "typescript",
15
+ js: "javascript",
16
+ jsx: "javascript",
17
+ mjs: "javascript",
18
+ cjs: "javascript",
19
+ json: "json",
20
+ md: "markdown",
21
+ html: "html",
22
+ htm: "html",
23
+ css: "css",
24
+ scss: "scss",
25
+ sass: "sass",
26
+ less: "less",
27
+ vue: "vue",
28
+ svelte: "svelte",
29
+ py: "python",
30
+ rb: "ruby",
31
+ rs: "rust",
32
+ go: "go",
33
+ java: "java",
34
+ kt: "kotlin",
35
+ swift: "swift",
36
+ c: "c",
37
+ cpp: "cpp",
38
+ h: "c",
39
+ hpp: "cpp",
40
+ cs: "csharp",
41
+ php: "php",
42
+ sql: "sql",
43
+ sh: "bash",
44
+ bash: "bash",
45
+ zsh: "bash",
46
+ ps1: "powershell",
47
+ yaml: "yaml",
48
+ yml: "yaml",
49
+ toml: "toml",
50
+ xml: "xml",
51
+ svg: "xml",
52
+ txt: "text"
53
+ });
54
+ //#endregion
55
+ //#region src/core/errors.ts
56
+ /** An invalid workspace edit or search operation. */
57
+ var WorkspaceError = class extends Error {
58
+ code;
59
+ context;
60
+ /**
61
+ * Create a workspace error.
62
+ *
63
+ * @param code - The machine-readable failure code
64
+ * @param message - The human-readable failure message
65
+ * @param context - Optional values associated with the failed operation
66
+ */
67
+ constructor(code, message, context) {
68
+ super(message);
69
+ this.name = "WorkspaceError";
70
+ this.code = code;
71
+ if (context !== void 0) this.context = context;
72
+ }
73
+ };
74
+ /**
75
+ * Narrow a caught value to a {@link WorkspaceError}.
76
+ *
77
+ * @param value - The caught value
78
+ * @returns Whether the value is a workspace error
79
+ *
80
+ * @example
81
+ * ```ts
82
+ * if (isWorkspaceError(error)) error.code
83
+ * ```
84
+ */
85
+ function isWorkspaceError(value) {
86
+ return value instanceof WorkspaceError;
87
+ }
88
+ //#endregion
89
+ //#region src/core/helpers.ts
90
+ /**
91
+ * Infer a language tag from the final file extension.
92
+ *
93
+ * @param path - The file path
94
+ * @returns The mapped language, or `text` when the extension is absent or unknown
95
+ *
96
+ * @example
97
+ * ```ts
98
+ * inferLanguage('src/main.ts') // 'typescript'
99
+ * ```
100
+ */
101
+ function inferLanguage(path) {
102
+ const dot = path.lastIndexOf(".");
103
+ if (dot === -1) return "text";
104
+ return EXTENSION_LANGUAGES[path.slice(dot + 1).toLowerCase()] ?? "text";
105
+ }
106
+ /**
107
+ * Determine whether content is the text arm.
108
+ *
109
+ * @param content - The file content
110
+ * @returns Whether the content carries text and a language
111
+ *
112
+ * @example
113
+ * ```ts
114
+ * isText({ text: 'hello', language: 'text' }) // true
115
+ * ```
116
+ */
117
+ function isText(content) {
118
+ return "text" in content;
119
+ }
120
+ /**
121
+ * Determine whether content is the binary arm.
122
+ *
123
+ * @param content - The file content
124
+ * @returns Whether the content carries base64 data and a MIME
125
+ *
126
+ * @example
127
+ * ```ts
128
+ * isBinary({ data: 'AAAA', mime: 'image/png' }) // true
129
+ * ```
130
+ */
131
+ function isBinary(content) {
132
+ return "data" in content;
133
+ }
134
+ /**
135
+ * Narrow an unknown value to an immutable file record.
136
+ *
137
+ * @param value - The value to inspect
138
+ * @returns Whether the value has the file shape
139
+ *
140
+ * @example
141
+ * ```ts
142
+ * isFile({ path: 'a.txt', content: { text: 'a', language: 'text' }, state: 'created', size: 1, lines: 1 })
143
+ * ```
144
+ */
145
+ function isFile(value) {
146
+ return (0, _orkestrel_contract.holds)(() => {
147
+ if (!(0, _orkestrel_contract.isRecord)(value)) return false;
148
+ if (!(0, _orkestrel_contract.isString)(value.path) || !(0, _orkestrel_contract.literalOf)("created", "modified")(value.state)) return false;
149
+ if (!(0, _orkestrel_contract.isNumber)(value.size) || !(0, _orkestrel_contract.isNumber)(value.lines)) return false;
150
+ if (!(0, _orkestrel_contract.isRecord)(value.content)) return false;
151
+ const text = (0, _orkestrel_contract.isString)(value.content.text) && (0, _orkestrel_contract.isString)(value.content.language);
152
+ const binary = (0, _orkestrel_contract.isString)(value.content.data) && (0, _orkestrel_contract.literalOf)("image/png", "image/jpeg", "image/gif", "image/webp")(value.content.mime);
153
+ return text || binary;
154
+ });
155
+ }
156
+ /**
157
+ * Narrow an unknown value to a workspace snapshot.
158
+ *
159
+ * @param value - The value read from a persistence boundary
160
+ * @returns Whether the value has the workspace snapshot shape
161
+ *
162
+ * @example
163
+ * ```ts
164
+ * isWorkspaceSnapshot({ id: 'work', files: [] }) // true
165
+ * ```
166
+ */
167
+ function isWorkspaceSnapshot(value) {
168
+ return (0, _orkestrel_contract.holds)(() => (0, _orkestrel_contract.isRecord)(value) && (0, _orkestrel_contract.isString)(value.id) && (0, _orkestrel_contract.arrayOf)(isFile)(value.files));
169
+ }
170
+ /**
171
+ * Compute the byte size of file content.
172
+ *
173
+ * @param content - The content to measure
174
+ * @returns UTF-8 bytes for text or decoded bytes for binary content
175
+ *
176
+ * @example
177
+ * ```ts
178
+ * computeSize({ text: 'café', language: 'text' }) // 5
179
+ * ```
180
+ */
181
+ function computeSize(content) {
182
+ if (isText(content)) return new TextEncoder().encode(content.text).length;
183
+ return decodedSize(content.data);
184
+ }
185
+ /**
186
+ * Count the lines in file content.
187
+ *
188
+ * @param content - The content to inspect
189
+ * @returns The text line count, or zero for empty text and binary content
190
+ *
191
+ * @example
192
+ * ```ts
193
+ * countLines({ text: 'a\nb', language: 'text' }) // 2
194
+ * ```
195
+ */
196
+ function countLines(content) {
197
+ if (!isText(content)) return 0;
198
+ if (content.text.length === 0) return 0;
199
+ let count = 1;
200
+ for (const character of content.text) if (character === "\n") count += 1;
201
+ return count;
202
+ }
203
+ /**
204
+ * Compute the decoded byte length of a base64 string.
205
+ *
206
+ * @param base64 - The base64 data
207
+ * @returns The decoded byte count
208
+ *
209
+ * @example
210
+ * ```ts
211
+ * decodedSize('AAAA') // 3
212
+ * ```
213
+ */
214
+ function decodedSize(base64) {
215
+ let padding = 0;
216
+ if (base64.endsWith("==")) padding = 2;
217
+ else if (base64.endsWith("=")) padding = 1;
218
+ return Math.floor(base64.length * 3 / 4) - padding;
219
+ }
220
+ /**
221
+ * Determine whether a 1-based range is structurally valid.
222
+ *
223
+ * @param range - The range to inspect
224
+ * @returns Whether both positions are positive and ordered
225
+ *
226
+ * @example
227
+ * ```ts
228
+ * isValidRange({ start: { line: 1, column: 1 }, end: { line: 1, column: 2 } }) // true
229
+ * ```
230
+ */
231
+ function isValidRange(range) {
232
+ if (range.start.line < 1 || range.start.column < 1) return false;
233
+ if (range.end.line < 1 || range.end.column < 1) return false;
234
+ if (range.start.line > range.end.line) return false;
235
+ return !(range.start.line === range.end.line && range.start.column > range.end.column);
236
+ }
237
+ /**
238
+ * Clamp a position to text bounds.
239
+ *
240
+ * @param text - The addressed text
241
+ * @param position - The requested position
242
+ * @returns The clamped position
243
+ *
244
+ * @example
245
+ * ```ts
246
+ * clampPosition('ab', { line: 9, column: 9 }) // { line: 1, column: 3 }
247
+ * ```
248
+ */
249
+ function clampPosition(text, position) {
250
+ const lines = text.split("\n");
251
+ const line = Math.max(1, Math.min(position.line, lines.length));
252
+ const lineText = lines[line - 1] ?? "";
253
+ return {
254
+ line,
255
+ column: Math.max(1, Math.min(position.column, lineText.length + 1))
256
+ };
257
+ }
258
+ /**
259
+ * Clamp both positions in a range to text bounds.
260
+ *
261
+ * @param text - The addressed text
262
+ * @param range - The requested range
263
+ * @returns The clamped range
264
+ *
265
+ * @example
266
+ * ```ts
267
+ * clampRange('ab', { start: { line: 1, column: 1 }, end: { line: 9, column: 9 } })
268
+ * ```
269
+ */
270
+ function clampRange(text, range) {
271
+ return {
272
+ start: clampPosition(text, range.start),
273
+ end: clampPosition(text, range.end)
274
+ };
275
+ }
276
+ /**
277
+ * Convert a 1-based position to a zero-based string offset.
278
+ *
279
+ * @param text - The addressed text
280
+ * @param position - The requested position
281
+ * @returns The bounded string offset
282
+ *
283
+ * @example
284
+ * ```ts
285
+ * offsetAt('ab\ncd', { line: 2, column: 1 }) // 3
286
+ * ```
287
+ */
288
+ function offsetAt(text, position) {
289
+ const lines = text.split("\n");
290
+ let offset = 0;
291
+ for (let index = 0; index < position.line - 1 && index < lines.length; index += 1) offset += (lines[index]?.length ?? 0) + 1;
292
+ offset += position.column - 1;
293
+ return Math.min(offset, text.length);
294
+ }
295
+ /**
296
+ * Slice a clamped half-open text range.
297
+ *
298
+ * @param text - The source text
299
+ * @param range - The requested range
300
+ * @returns The ranged substring
301
+ *
302
+ * @example
303
+ * ```ts
304
+ * sliceRange('hello', { start: { line: 1, column: 1 }, end: { line: 1, column: 6 } }) // 'hello'
305
+ * ```
306
+ */
307
+ function sliceRange(text, range) {
308
+ const clamped = clampRange(text, range);
309
+ return text.slice(offsetAt(text, clamped.start), offsetAt(text, clamped.end));
310
+ }
311
+ /**
312
+ * Replace a clamped half-open text range.
313
+ *
314
+ * @param text - The source text
315
+ * @param range - The requested range
316
+ * @param replacement - The replacement text
317
+ * @returns The spliced text
318
+ *
319
+ * @example
320
+ * ```ts
321
+ * spliceRange('hello', { start: { line: 1, column: 1 }, end: { line: 1, column: 6 } }, 'bye')
322
+ * ```
323
+ */
324
+ function spliceRange(text, range, replacement) {
325
+ const clamped = clampRange(text, range);
326
+ const start = offsetAt(text, clamped.start);
327
+ const end = offsetAt(text, clamped.end);
328
+ return text.slice(0, start) + replacement + text.slice(end);
329
+ }
330
+ /**
331
+ * Assemble a nested range from four 1-based coordinates.
332
+ *
333
+ * @param fromLine - The start line
334
+ * @param fromColumn - The start column
335
+ * @param toLine - The end line
336
+ * @param toColumn - The end column
337
+ * @returns The assembled range without validation
338
+ *
339
+ * @example
340
+ * ```ts
341
+ * rangeOf(1, 1, 1, 2)
342
+ * ```
343
+ */
344
+ function rangeOf(fromLine, fromColumn, toLine, toColumn) {
345
+ return {
346
+ start: {
347
+ line: fromLine,
348
+ column: fromColumn
349
+ },
350
+ end: {
351
+ line: toLine,
352
+ column: toColumn
353
+ }
354
+ };
355
+ }
356
+ /**
357
+ * Escape regular-expression metacharacters for literal matching.
358
+ *
359
+ * @param value - The literal text
360
+ * @returns The escaped regular-expression source
361
+ *
362
+ * @example
363
+ * ```ts
364
+ * escapeRegExp('a.b') // 'a\\.b'
365
+ * ```
366
+ */
367
+ function escapeRegExp(value) {
368
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
369
+ }
370
+ //#endregion
371
+ //#region src/core/workspaces/Workspace.ts
372
+ /**
373
+ * A mutable path-keyed editing surface over immutable files.
374
+ *
375
+ * Whole-file edits create text files, ranged edits operate only on existing text files, and
376
+ * binary files remain available through construction-time hydration. Mutations emit after the
377
+ * registry has changed.
378
+ *
379
+ * @example
380
+ * ```ts
381
+ * import { Workspace } from '@orkestrel/workspace'
382
+ *
383
+ * const workspace = new Workspace()
384
+ * workspace.write('notes.txt', 'hello')
385
+ * workspace.append('notes.txt', ' world')
386
+ * workspace.read('notes.txt') // 'hello world'
387
+ * ```
388
+ */
389
+ var Workspace = class {
390
+ #id;
391
+ #files = /* @__PURE__ */ new Map();
392
+ #emitter;
393
+ /**
394
+ * Create a workspace.
395
+ *
396
+ * @param options - Optional identity, emitter configuration, and initial files
397
+ */
398
+ constructor(options) {
399
+ this.#id = options?.id ?? crypto.randomUUID();
400
+ this.#emitter = new _orkestrel_emitter.Emitter({
401
+ ...options?.on === void 0 ? {} : { on: options.on },
402
+ ...options?.error === void 0 ? {} : { error: options.error }
403
+ });
404
+ if (options?.seed) for (const file of options.seed) this.#files.set(file.path, file);
405
+ }
406
+ get id() {
407
+ return this.#id;
408
+ }
409
+ get emitter() {
410
+ return this.#emitter;
411
+ }
412
+ get count() {
413
+ return this.#files.size;
414
+ }
415
+ file(path) {
416
+ return this.#files.get(path);
417
+ }
418
+ files() {
419
+ return [...this.#files.values()];
420
+ }
421
+ read(path, range) {
422
+ if ((0, _orkestrel_contract.isArray)(path)) {
423
+ const result = {};
424
+ for (const one of path) {
425
+ const file = this.#files.get(one);
426
+ if (file && isText(file.content)) result[one] = file.content.text;
427
+ }
428
+ return result;
429
+ }
430
+ const file = this.#files.get(path);
431
+ if (!file) return void 0;
432
+ if (!range) return isText(file.content) ? file.content.text : void 0;
433
+ if (!isText(file.content)) throw new WorkspaceError("MODALITY", `Cannot read a range of a binary file: ${path}`, { path });
434
+ return {
435
+ content: sliceRange(file.content.text, range),
436
+ range: clampRange(file.content.text, range)
437
+ };
438
+ }
439
+ has(path) {
440
+ if ((0, _orkestrel_contract.isArray)(path)) return path.some((one) => this.#files.has(one));
441
+ return this.#files.has(path);
442
+ }
443
+ search(query, options) {
444
+ const pattern = this.#pattern(query, options);
445
+ const limit = options?.limit;
446
+ const matches = [];
447
+ for (const file of this.#files.values()) {
448
+ if (limit !== void 0 && matches.length >= limit) break;
449
+ if (!isText(file.content)) continue;
450
+ const lines = file.content.text.split("\n");
451
+ for (let index = 0; index < lines.length; index += 1) {
452
+ if (limit !== void 0 && matches.length >= limit) break;
453
+ const lineText = lines[index] ?? "";
454
+ pattern.lastIndex = 0;
455
+ let hit = pattern.exec(lineText);
456
+ while (hit !== null) {
457
+ if (limit !== void 0 && matches.length >= limit) break;
458
+ matches.push({
459
+ path: file.path,
460
+ line: index + 1,
461
+ column: hit.index + 1,
462
+ length: hit[0].length,
463
+ content: lineText
464
+ });
465
+ if (hit[0].length === 0) pattern.lastIndex += 1;
466
+ hit = pattern.exec(lineText);
467
+ }
468
+ }
469
+ }
470
+ return matches;
471
+ }
472
+ replace(query, replacement, options) {
473
+ const pattern = this.#pattern(query, options);
474
+ const limit = options?.limit;
475
+ let occurrences = 0;
476
+ let files = 0;
477
+ for (const [path, file] of this.#files) {
478
+ if (limit !== void 0 && occurrences >= limit) break;
479
+ if (!isText(file.content)) continue;
480
+ const remaining = limit === void 0 ? void 0 : limit - occurrences;
481
+ let count = 0;
482
+ pattern.lastIndex = 0;
483
+ const next = file.content.text.replace(pattern, (match) => {
484
+ if (remaining !== void 0 && count >= remaining) return match;
485
+ count += 1;
486
+ return replacement;
487
+ });
488
+ if (count > 0) {
489
+ occurrences += count;
490
+ files += 1;
491
+ this.write(path, next);
492
+ }
493
+ }
494
+ return {
495
+ occurrences,
496
+ files
497
+ };
498
+ }
499
+ write(path, content, range) {
500
+ if ((0, _orkestrel_contract.isRecord)(path)) {
501
+ for (const [one, text] of Object.entries(path)) this.#write(one, text);
502
+ return;
503
+ }
504
+ const text = content ?? "";
505
+ if (!range) {
506
+ this.#write(path, text);
507
+ return;
508
+ }
509
+ this.#splice(path, text, range);
510
+ }
511
+ prepend(path, content) {
512
+ if ((0, _orkestrel_contract.isRecord)(path)) {
513
+ for (const [one, text] of Object.entries(path)) this.#prepend(one, text);
514
+ return;
515
+ }
516
+ this.#prepend(path, content ?? "");
517
+ }
518
+ append(path, content) {
519
+ if ((0, _orkestrel_contract.isRecord)(path)) {
520
+ for (const [one, text] of Object.entries(path)) this.#append(one, text);
521
+ return;
522
+ }
523
+ this.#append(path, content ?? "");
524
+ }
525
+ move(from, to) {
526
+ if ((0, _orkestrel_contract.isRecord)(from)) {
527
+ let moved = false;
528
+ for (const [one, target] of Object.entries(from)) if (this.#move(one, target)) moved = true;
529
+ return moved;
530
+ }
531
+ return this.#move(from, to ?? "");
532
+ }
533
+ remove(path) {
534
+ if ((0, _orkestrel_contract.isArray)(path)) {
535
+ let removed = false;
536
+ for (const one of path) if (this.#remove(one)) removed = true;
537
+ return removed;
538
+ }
539
+ return this.#remove(path);
540
+ }
541
+ clear() {
542
+ this.#files.clear();
543
+ this.#emitter.emit("clear");
544
+ }
545
+ snapshot() {
546
+ return {
547
+ id: this.#id,
548
+ files: this.files()
549
+ };
550
+ }
551
+ destroy() {
552
+ this.#emitter.destroy();
553
+ }
554
+ #write(path, content) {
555
+ const existing = this.#files.get(path);
556
+ const file = createFile({
557
+ path,
558
+ content: {
559
+ text: content,
560
+ language: existing && isText(existing.content) ? existing.content.language : inferLanguage(path)
561
+ },
562
+ state: existing ? "modified" : "created"
563
+ });
564
+ this.#files.set(path, file);
565
+ this.#emitter.emit("write", file);
566
+ }
567
+ #splice(path, content, range) {
568
+ const existing = this.#files.get(path);
569
+ if (!existing) throw new WorkspaceError("MODALITY", `Cannot splice a range of a missing file: ${path}`, { path });
570
+ if (!isText(existing.content)) throw new WorkspaceError("MODALITY", `Cannot splice a range of a binary file: ${path}`, { path });
571
+ if (!isValidRange(range)) throw new WorkspaceError("RANGE", `Invalid range for file: ${path}`, {
572
+ path,
573
+ range
574
+ });
575
+ const file = createFile({
576
+ path,
577
+ content: {
578
+ text: spliceRange(existing.content.text, range, content),
579
+ language: existing.content.language
580
+ },
581
+ state: "modified"
582
+ });
583
+ this.#files.set(path, file);
584
+ this.#emitter.emit("write", file);
585
+ }
586
+ #prepend(path, content) {
587
+ const existing = this.#files.get(path);
588
+ let text = "";
589
+ if (existing) {
590
+ if (!isText(existing.content)) throw new WorkspaceError("MODALITY", `Cannot prepend text to a binary file: ${path}`, { path });
591
+ text = existing.content.text;
592
+ }
593
+ this.#write(path, content + text);
594
+ }
595
+ #append(path, content) {
596
+ const existing = this.#files.get(path);
597
+ let text = "";
598
+ if (existing) {
599
+ if (!isText(existing.content)) throw new WorkspaceError("MODALITY", `Cannot append text to a binary file: ${path}`, { path });
600
+ text = existing.content.text;
601
+ }
602
+ this.#write(path, text + content);
603
+ }
604
+ #move(from, to) {
605
+ if (from === to) return false;
606
+ const file = this.#files.get(from);
607
+ if (!file) return false;
608
+ const moved = createFile({
609
+ path: to,
610
+ content: file.content,
611
+ state: "modified"
612
+ });
613
+ const entries = [...this.#files];
614
+ this.#files.clear();
615
+ for (const [path, current] of entries) if (path === from) this.#files.set(to, moved);
616
+ else if (path !== to) this.#files.set(path, current);
617
+ this.#emitter.emit("move", from, to);
618
+ return true;
619
+ }
620
+ #remove(path) {
621
+ const removed = this.#files.delete(path);
622
+ if (removed) this.#emitter.emit("remove", path);
623
+ return removed;
624
+ }
625
+ #pattern(query, options) {
626
+ const source = options?.regex === true ? query : escapeRegExp(query);
627
+ const flags = options?.sensitive === false ? "gi" : "g";
628
+ try {
629
+ return new RegExp(source, flags);
630
+ } catch {
631
+ throw new WorkspaceError("PATTERN", `Invalid search pattern: ${query}`, { query });
632
+ }
633
+ }
634
+ };
635
+ //#endregion
636
+ //#region src/core/workspaces/WorkspaceManager.ts
637
+ /**
638
+ * An insertion-ordered workspace registry with an active selection.
639
+ *
640
+ * A supplied store adds lenient snapshot `open` and `save` operations. Event defaults flow into
641
+ * workspaces created through the registry, while observability remains owned by each workspace.
642
+ *
643
+ * @example
644
+ * ```ts
645
+ * import { WorkspaceManager } from '@orkestrel/workspace'
646
+ *
647
+ * const manager = new WorkspaceManager()
648
+ * const workspace = manager.add({ id: 'scratch' })
649
+ * manager.active === workspace
650
+ * ```
651
+ */
652
+ var WorkspaceManager = class {
653
+ #workspaces = /* @__PURE__ */ new Map();
654
+ #active;
655
+ #on;
656
+ #error;
657
+ #store;
658
+ /**
659
+ * Create a registry.
660
+ *
661
+ * @param options - Default event hooks and optional snapshot store
662
+ */
663
+ constructor(options) {
664
+ this.#on = options?.on;
665
+ this.#error = options?.error;
666
+ this.#store = options?.store;
667
+ }
668
+ get count() {
669
+ return this.#workspaces.size;
670
+ }
671
+ get active() {
672
+ return this.#active === void 0 ? void 0 : this.#workspaces.get(this.#active);
673
+ }
674
+ workspace(id) {
675
+ return this.#workspaces.get(id);
676
+ }
677
+ workspaces() {
678
+ return [...this.#workspaces.values()];
679
+ }
680
+ add(options) {
681
+ const on = options?.on ?? this.#on;
682
+ const error = options?.error ?? this.#error;
683
+ const workspace = new Workspace({
684
+ ...options?.id === void 0 ? {} : { id: options.id },
685
+ ...on === void 0 ? {} : { on },
686
+ ...error === void 0 ? {} : { error },
687
+ ...options?.seed === void 0 ? {} : { seed: options.seed }
688
+ });
689
+ this.#workspaces.set(workspace.id, workspace);
690
+ if (this.#active === void 0) this.#active = workspace.id;
691
+ return workspace;
692
+ }
693
+ switch(id) {
694
+ const workspace = this.#workspaces.get(id);
695
+ if (workspace === void 0) return void 0;
696
+ this.#active = id;
697
+ return workspace;
698
+ }
699
+ async open(id) {
700
+ const existing = this.#workspaces.get(id);
701
+ if (existing !== void 0) {
702
+ this.#active = id;
703
+ return existing;
704
+ }
705
+ if (this.#store === void 0) return void 0;
706
+ const snapshot = await this.#store.get(id);
707
+ if (snapshot === void 0) return void 0;
708
+ const workspace = this.add({
709
+ id,
710
+ seed: snapshot.files
711
+ });
712
+ this.#active = workspace.id;
713
+ return workspace;
714
+ }
715
+ async save(id) {
716
+ const workspace = this.#workspaces.get(id);
717
+ if (this.#store === void 0 || workspace === void 0) return false;
718
+ await this.#store.set(workspace.snapshot());
719
+ return true;
720
+ }
721
+ remove(ids) {
722
+ if ((0, _orkestrel_contract.isArray)(ids)) {
723
+ let removed = false;
724
+ for (const id of ids) if (this.#drop(id)) removed = true;
725
+ return removed;
726
+ }
727
+ return this.#drop(ids);
728
+ }
729
+ clear() {
730
+ for (const workspace of this.#workspaces.values()) workspace.destroy();
731
+ this.#workspaces.clear();
732
+ this.#active = void 0;
733
+ }
734
+ #drop(id) {
735
+ const workspace = this.#workspaces.get(id);
736
+ if (workspace === void 0) return false;
737
+ this.#workspaces.delete(id);
738
+ workspace.destroy();
739
+ if (this.#active === id) this.#active = void 0;
740
+ return true;
741
+ }
742
+ };
743
+ //#endregion
744
+ //#region src/core/workspaces/stores/DatabaseWorkspaceStore.ts
745
+ /**
746
+ * A workspace snapshot store over a database table.
747
+ *
748
+ * Snapshots occupy one opaque column and are narrowed when read back from the storage boundary.
749
+ *
750
+ * @example
751
+ * ```ts
752
+ * import { createDatabaseWorkspaceStore } from '@orkestrel/workspace'
753
+ *
754
+ * const store = createDatabaseWorkspaceStore()
755
+ * ```
756
+ */
757
+ var DatabaseWorkspaceStore = class {
758
+ #table;
759
+ /**
760
+ * Wrap a workspace snapshot table.
761
+ *
762
+ * @param table - The table keyed by workspace identifier
763
+ */
764
+ constructor(table) {
765
+ this.#table = table;
766
+ }
767
+ async get(id) {
768
+ const row = await this.#table.get(id);
769
+ if (row === void 0) return void 0;
770
+ return isWorkspaceSnapshot(row.snapshot) ? row.snapshot : void 0;
771
+ }
772
+ async set(snapshot) {
773
+ await this.#table.set({
774
+ id: snapshot.id,
775
+ snapshot
776
+ });
777
+ }
778
+ async delete(id) {
779
+ await this.#table.remove(id);
780
+ }
781
+ };
782
+ //#endregion
783
+ //#region src/core/workspaces/stores/MemoryWorkspaceStore.ts
784
+ /**
785
+ * A process-local workspace snapshot store.
786
+ *
787
+ * @example
788
+ * ```ts
789
+ * import { MemoryWorkspaceStore } from '@orkestrel/workspace'
790
+ *
791
+ * const store = new MemoryWorkspaceStore()
792
+ * ```
793
+ */
794
+ var MemoryWorkspaceStore = class {
795
+ #snapshots = /* @__PURE__ */ new Map();
796
+ get(id) {
797
+ return Promise.resolve(this.#snapshots.get(id));
798
+ }
799
+ set(snapshot) {
800
+ this.#snapshots.set(snapshot.id, snapshot);
801
+ return Promise.resolve();
802
+ }
803
+ delete(id) {
804
+ this.#snapshots.delete(id);
805
+ return Promise.resolve();
806
+ }
807
+ };
808
+ //#endregion
809
+ //#region src/core/factories.ts
810
+ /**
811
+ * Create an immutable file with derived size and line counts.
812
+ *
813
+ * @param input - The file path, content, and optional state
814
+ * @returns A frozen file record
815
+ *
816
+ * @example
817
+ * ```ts
818
+ * import { createFile, createTextContent } from '@orkestrel/workspace'
819
+ *
820
+ * createFile({ path: 'a.txt', content: createTextContent('hello', 'text') })
821
+ * ```
822
+ */
823
+ function createFile(input) {
824
+ return Object.freeze({
825
+ path: input.path,
826
+ content: input.content,
827
+ state: input.state ?? "created",
828
+ size: computeSize(input.content),
829
+ lines: countLines(input.content)
830
+ });
831
+ }
832
+ /**
833
+ * Create the text arm of {@link FileContent}.
834
+ *
835
+ * @param text - The text body
836
+ * @param language - The language tag
837
+ * @returns Text file content
838
+ *
839
+ * @example
840
+ * ```ts
841
+ * import { createTextContent } from '@orkestrel/workspace'
842
+ *
843
+ * createTextContent('hello', 'text')
844
+ * ```
845
+ */
846
+ function createTextContent(text, language) {
847
+ return {
848
+ text,
849
+ language
850
+ };
851
+ }
852
+ /**
853
+ * Create the binary arm of {@link FileContent}.
854
+ *
855
+ * @param data - The base64 payload
856
+ * @param mime - The binary MIME
857
+ * @returns Binary file content
858
+ *
859
+ * @example
860
+ * ```ts
861
+ * import { createBinaryContent } from '@orkestrel/workspace'
862
+ *
863
+ * createBinaryContent('AAAA', 'image/png')
864
+ * ```
865
+ */
866
+ function createBinaryContent(data, mime) {
867
+ return {
868
+ data,
869
+ mime
870
+ };
871
+ }
872
+ /**
873
+ * Create a workspace.
874
+ *
875
+ * @param options - Optional identity, emitter configuration, and initial files
876
+ * @returns A working workspace
877
+ *
878
+ * @example
879
+ * ```ts
880
+ * import { createWorkspace } from '@orkestrel/workspace'
881
+ *
882
+ * const workspace = createWorkspace()
883
+ * workspace.write('a.txt', 'hello')
884
+ * ```
885
+ */
886
+ function createWorkspace(options) {
887
+ return new Workspace(options);
888
+ }
889
+ /**
890
+ * Create an in-memory workspace snapshot store.
891
+ *
892
+ * @returns A process-local workspace store
893
+ *
894
+ * @example
895
+ * ```ts
896
+ * import { createMemoryWorkspaceStore } from '@orkestrel/workspace'
897
+ *
898
+ * const store = createMemoryWorkspaceStore()
899
+ * ```
900
+ */
901
+ function createMemoryWorkspaceStore() {
902
+ return new MemoryWorkspaceStore();
903
+ }
904
+ /**
905
+ * Create a database-backed workspace snapshot store.
906
+ *
907
+ * @param driver - The database driver; defaults to an in-memory driver
908
+ * @returns A workspace store backed by the supplied driver
909
+ *
910
+ * @example
911
+ * ```ts
912
+ * import { createDatabaseWorkspaceStore } from '@orkestrel/workspace'
913
+ *
914
+ * const store = createDatabaseWorkspaceStore()
915
+ * ```
916
+ */
917
+ function createDatabaseWorkspaceStore(driver = (0, _orkestrel_database.createMemoryDriver)()) {
918
+ const columns = {
919
+ id: (0, _orkestrel_contract.stringShape)(),
920
+ snapshot: (0, _orkestrel_contract.rawShape)({})
921
+ };
922
+ return new DatabaseWorkspaceStore((0, _orkestrel_database.createDatabase)({
923
+ driver,
924
+ tables: { workspaces: columns }
925
+ }).table("workspaces"));
926
+ }
927
+ /**
928
+ * Create an empty workspace registry.
929
+ *
930
+ * @param options - Default event hooks and optional durability
931
+ * @returns A workspace manager
932
+ *
933
+ * @example
934
+ * ```ts
935
+ * import { createWorkspaceManager } from '@orkestrel/workspace'
936
+ *
937
+ * const manager = createWorkspaceManager()
938
+ * manager.add()
939
+ * ```
940
+ */
941
+ function createWorkspaceManager(options) {
942
+ return new WorkspaceManager(options);
943
+ }
944
+ //#endregion
945
+ exports.DatabaseWorkspaceStore = DatabaseWorkspaceStore;
946
+ exports.EXTENSION_LANGUAGES = EXTENSION_LANGUAGES;
947
+ exports.MemoryWorkspaceStore = MemoryWorkspaceStore;
948
+ exports.Workspace = Workspace;
949
+ exports.WorkspaceError = WorkspaceError;
950
+ exports.WorkspaceManager = WorkspaceManager;
951
+ exports.clampPosition = clampPosition;
952
+ exports.clampRange = clampRange;
953
+ exports.computeSize = computeSize;
954
+ exports.countLines = countLines;
955
+ exports.createBinaryContent = createBinaryContent;
956
+ exports.createDatabaseWorkspaceStore = createDatabaseWorkspaceStore;
957
+ exports.createFile = createFile;
958
+ exports.createMemoryWorkspaceStore = createMemoryWorkspaceStore;
959
+ exports.createTextContent = createTextContent;
960
+ exports.createWorkspace = createWorkspace;
961
+ exports.createWorkspaceManager = createWorkspaceManager;
962
+ exports.decodedSize = decodedSize;
963
+ exports.escapeRegExp = escapeRegExp;
964
+ exports.inferLanguage = inferLanguage;
965
+ exports.isBinary = isBinary;
966
+ exports.isFile = isFile;
967
+ exports.isText = isText;
968
+ exports.isValidRange = isValidRange;
969
+ exports.isWorkspaceError = isWorkspaceError;
970
+ exports.isWorkspaceSnapshot = isWorkspaceSnapshot;
971
+ exports.offsetAt = offsetAt;
972
+ exports.rangeOf = rangeOf;
973
+ exports.sliceRange = sliceRange;
974
+ exports.spliceRange = spliceRange;
975
+
976
+ //# sourceMappingURL=index.cjs.map