ducjs 3.0.5 → 3.1.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.
@@ -279,7 +279,7 @@ export function readVersionGraph(duc_buf) {
279
279
  * @returns {number}
280
280
  */
281
281
  export function getCurrentSchemaVersion() {
282
- return 3000000;
282
+ return 3000001;
283
283
  }
284
284
 
285
285
  /**
Binary file
@@ -1,4 +1,4 @@
1
- import type { DucExternalFileData, DucExternalFiles } from "./types";
1
+ import type { DucExternalFile, DucExternalFiles, ExternalFileRevision } from "./types";
2
2
  export type LazyFileMetadata = {
3
3
  id: string;
4
4
  mimeType: string;
@@ -26,17 +26,19 @@ export declare class LazyExternalFileStore {
26
26
  getMetadata(fileId: string): LazyFileMetadata | undefined;
27
27
  /** Get metadata for all files. */
28
28
  getAllMetadata(): LazyFileMetadata[];
29
- /** Fetch the full file data (including blob) for a specific file. */
30
- getFileData(fileId: string): DucExternalFileData | null;
31
- /** Fetch file data and return a copy of the data buffer (safe for transfer). */
32
- getFileDataCopy(fileId: string): DucExternalFileData | null;
29
+ /** Fetch the full file (including data blobs for all revisions) for a specific file. */
30
+ getFile(fileId: string): DucExternalFile | null;
31
+ /** Get the active revision data for a specific file. */
32
+ getFileData(fileId: string): ExternalFileRevision | null;
33
+ /** Fetch active revision data and return a copy of the data buffer (safe for transfer). */
34
+ getFileDataCopy(fileId: string): ExternalFileRevision | null;
33
35
  /** Add a file at runtime (not persisted in .duc until next serialize). */
34
- addRuntimeFile(fileId: string, data: DucExternalFileData): void;
36
+ addRuntimeFile(fileId: string, file: DucExternalFile): void;
35
37
  /** Remove a runtime file. */
36
38
  removeRuntimeFile(fileId: string): boolean;
37
39
  /** Export all files eagerly as a DucExternalFiles record. */
38
40
  toExternalFiles(): DucExternalFiles;
39
- /** Merge files from another source (only adds missing). */
41
+ /** Merge files from another source. Adds missing files and merges new revisions into existing ones. */
40
42
  mergeFiles(files: DucExternalFiles): void;
41
43
  /** Release the underlying buffer to free memory. */
42
44
  release(): void;
@@ -26,11 +26,14 @@ export class LazyExternalFileStore {
26
26
  getMetadata(fileId) {
27
27
  const rt = this.runtimeFiles.get(fileId);
28
28
  if (rt) {
29
+ const active = rt.revisions[rt.activeRevisionId];
30
+ if (!active)
31
+ return undefined;
29
32
  return {
30
33
  id: rt.id,
31
- mimeType: rt.mimeType,
32
- created: rt.created,
33
- lastRetrieved: rt.lastRetrieved,
34
+ mimeType: active.mimeType,
35
+ created: active.created,
36
+ lastRetrieved: active.lastRetrieved,
34
37
  version: rt.version,
35
38
  };
36
39
  }
@@ -39,24 +42,28 @@ export class LazyExternalFileStore {
39
42
  /** Get metadata for all files. */
40
43
  getAllMetadata() {
41
44
  const result = [];
42
- for (const meta of this.getMetadataMap().values()) {
45
+ const persisted = this.getMetadataMap();
46
+ for (const meta of persisted.values()) {
43
47
  result.push(meta);
44
48
  }
45
- for (const [id, data] of this.runtimeFiles) {
46
- if (!this.getMetadataMap().has(id)) {
47
- result.push({
48
- id: data.id,
49
- mimeType: data.mimeType,
50
- created: data.created,
51
- lastRetrieved: data.lastRetrieved,
52
- version: data.version,
53
- });
49
+ for (const [id, file] of this.runtimeFiles) {
50
+ if (!persisted.has(id)) {
51
+ const active = file.revisions[file.activeRevisionId];
52
+ if (active) {
53
+ result.push({
54
+ id: file.id,
55
+ mimeType: active.mimeType,
56
+ created: active.created,
57
+ lastRetrieved: active.lastRetrieved,
58
+ version: file.version,
59
+ });
60
+ }
54
61
  }
55
62
  }
56
63
  return result;
57
64
  }
58
- /** Fetch the full file data (including blob) for a specific file. */
59
- getFileData(fileId) {
65
+ /** Fetch the full file (including data blobs for all revisions) for a specific file. */
66
+ getFile(fileId) {
60
67
  const rt = this.runtimeFiles.get(fileId);
61
68
  if (rt)
62
69
  return rt;
@@ -65,15 +72,17 @@ export class LazyExternalFileStore {
65
72
  const result = wasmGetExternalFile(this.buffer, fileId);
66
73
  if (!result)
67
74
  return null;
68
- // WASM returns DucExternalFileEntry { key, value: DucExternalFileData }.
69
- // Unwrap to get the actual file data.
70
- const entry = result;
71
- if (entry.value && typeof entry.value === "object") {
72
- return entry.value;
73
- }
74
- return entry;
75
+ return result;
75
76
  }
76
- /** Fetch file data and return a copy of the data buffer (safe for transfer). */
77
+ /** Get the active revision data for a specific file. */
78
+ getFileData(fileId) {
79
+ var _a;
80
+ const file = this.getFile(fileId);
81
+ if (!file)
82
+ return null;
83
+ return (_a = file.revisions[file.activeRevisionId]) !== null && _a !== void 0 ? _a : null;
84
+ }
85
+ /** Fetch active revision data and return a copy of the data buffer (safe for transfer). */
77
86
  getFileDataCopy(fileId) {
78
87
  const data = this.getFileData(fileId);
79
88
  if (!data)
@@ -81,8 +90,8 @@ export class LazyExternalFileStore {
81
90
  return Object.assign(Object.assign({}, data), { data: new Uint8Array(data.data) });
82
91
  }
83
92
  /** Add a file at runtime (not persisted in .duc until next serialize). */
84
- addRuntimeFile(fileId, data) {
85
- this.runtimeFiles.set(fileId, data);
93
+ addRuntimeFile(fileId, file) {
94
+ this.runtimeFiles.set(fileId, file);
86
95
  }
87
96
  /** Remove a runtime file. */
88
97
  removeRuntimeFile(fileId) {
@@ -92,24 +101,42 @@ export class LazyExternalFileStore {
92
101
  toExternalFiles() {
93
102
  const result = {};
94
103
  if (this.buffer) {
95
- const metas = this.getMetadataMap();
96
- for (const [id] of metas) {
97
- const data = this.getFileData(id);
98
- if (data) {
99
- result[id] = data;
104
+ for (const [id] of this.getMetadataMap()) {
105
+ const file = this.getFile(id);
106
+ if (file) {
107
+ result[id] = file;
100
108
  }
101
109
  }
102
110
  }
103
- for (const [id, data] of this.runtimeFiles) {
104
- result[id] = data;
111
+ for (const [id, file] of this.runtimeFiles) {
112
+ result[id] = file;
105
113
  }
106
114
  return result;
107
115
  }
108
- /** Merge files from another source (only adds missing). */
116
+ /** Merge files from another source. Adds missing files and merges new revisions into existing ones. */
109
117
  mergeFiles(files) {
110
- for (const [id, data] of Object.entries(files)) {
118
+ var _a, _b, _c;
119
+ for (const [id, file] of Object.entries(files)) {
111
120
  if (!this.has(id)) {
112
- this.runtimeFiles.set(id, data);
121
+ this.runtimeFiles.set(id, file);
122
+ continue;
123
+ }
124
+ const existing = (_a = this.runtimeFiles.get(id)) !== null && _a !== void 0 ? _a : this.getFile(id);
125
+ if (!existing) {
126
+ this.runtimeFiles.set(id, file);
127
+ continue;
128
+ }
129
+ // Merge: add any new revisions that don't exist yet, and update metadata
130
+ let merged = false;
131
+ const mergedRevisions = Object.assign({}, existing.revisions);
132
+ for (const [revId, rev] of Object.entries(file.revisions)) {
133
+ if (!mergedRevisions[revId]) {
134
+ mergedRevisions[revId] = rev;
135
+ merged = true;
136
+ }
137
+ }
138
+ if (merged || file.updated > existing.updated) {
139
+ this.runtimeFiles.set(id, Object.assign(Object.assign({}, existing), { activeRevisionId: file.activeRevisionId, updated: Math.max(file.updated, existing.updated), version: Math.max((_b = file.version) !== null && _b !== void 0 ? _b : 0, (_c = existing.version) !== null && _c !== void 0 ? _c : 0), revisions: mergedRevisions }));
113
140
  }
114
141
  }
115
142
  }
@@ -41,46 +41,94 @@ export const restore = (data, elementsConfig, restoreConfig = {}) => {
41
41
  };
42
42
  };
43
43
  export const restoreFiles = (importedFiles) => {
44
- var _a;
44
+ var _a, _b;
45
45
  if (!importedFiles || typeof importedFiles !== "object") {
46
46
  return {};
47
47
  }
48
48
  const restoredFiles = {};
49
49
  const files = importedFiles;
50
50
  for (const key in files) {
51
- if (Object.prototype.hasOwnProperty.call(files, key)) {
52
- let fileData = files[key];
53
- if (!fileData || typeof fileData !== "object") {
51
+ if (!Object.prototype.hasOwnProperty.call(files, key))
52
+ continue;
53
+ const fileData = files[key];
54
+ if (!fileData || typeof fileData !== "object")
55
+ continue;
56
+ const fd = fileData;
57
+ // New format: DucExternalFile with revisions map
58
+ if (fd.revisions && typeof fd.revisions === "object" && fd.activeRevisionId) {
59
+ const id = isValidExternalFileId(fd.id);
60
+ if (!id)
54
61
  continue;
55
- }
56
- // Handle the nested DucExternalFileEntry structure { key, value: { ... } }
57
- // produced by Rust serde or legacy exports.
58
- if (fileData.value && typeof fileData.value === "object") {
59
- fileData = fileData.value;
60
- }
61
- const id = isValidExternalFileId(fileData.id);
62
- const mimeType = isValidString(fileData.mimeType);
63
- const created = isFiniteNumber(fileData.created)
64
- ? fileData.created
65
- : Date.now();
66
- // Check for data under 'data' or 'dataURL' to be more flexible.
67
- const dataSource = (_a = fileData.data) !== null && _a !== void 0 ? _a : fileData.dataURL;
68
- const data = isValidUint8Array(dataSource);
69
- if (id && mimeType && data) {
70
- restoredFiles[id] = {
71
- id,
62
+ const restoredRevisions = {};
63
+ const rawRevisions = fd.revisions;
64
+ for (const revKey in rawRevisions) {
65
+ if (!Object.prototype.hasOwnProperty.call(rawRevisions, revKey))
66
+ continue;
67
+ const rev = rawRevisions[revKey];
68
+ if (!rev || typeof rev !== "object")
69
+ continue;
70
+ const r = rev;
71
+ const revId = isValidString(r.id);
72
+ const mimeType = isValidString(r.mimeType);
73
+ const dataSource = (_a = r.data) !== null && _a !== void 0 ? _a : r.dataURL;
74
+ const data = isValidUint8Array(dataSource);
75
+ if (!revId || !mimeType || !data)
76
+ continue;
77
+ restoredRevisions[revKey] = {
78
+ id: revId,
79
+ sizeBytes: isFiniteNumber(r.sizeBytes) ? r.sizeBytes : data.byteLength,
80
+ checksum: isValidString(r.checksum) || undefined,
81
+ sourceName: isValidString(r.sourceName) || undefined,
72
82
  mimeType,
83
+ message: isValidString(r.message) || undefined,
84
+ created: isFiniteNumber(r.created) ? r.created : Date.now(),
85
+ lastRetrieved: isFiniteNumber(r.lastRetrieved) ? r.lastRetrieved : undefined,
73
86
  data,
74
- created,
75
- lastRetrieved: isFiniteNumber(fileData.lastRetrieved)
76
- ? fileData.lastRetrieved
77
- : undefined,
78
- version: isFiniteNumber(fileData.version)
79
- ? fileData.version
80
- : undefined,
81
87
  };
82
88
  }
89
+ if (Object.keys(restoredRevisions).length === 0)
90
+ continue;
91
+ restoredFiles[id] = {
92
+ id,
93
+ activeRevisionId: isValidString(fd.activeRevisionId) || Object.keys(restoredRevisions)[0],
94
+ updated: isFiniteNumber(fd.updated) ? fd.updated : Date.now(),
95
+ revisions: restoredRevisions,
96
+ version: isFiniteNumber(fd.version) ? fd.version : undefined,
97
+ };
98
+ continue;
83
99
  }
100
+ // Legacy flat format: DucExternalFileData — wrap in a single-revision DucExternalFile.
101
+ let legacyData = fd;
102
+ // Handle the nested { key, value: { ... } } structure from old Rust serde output.
103
+ if (fd.value && typeof fd.value === "object") {
104
+ legacyData = fd.value;
105
+ }
106
+ const id = isValidExternalFileId(legacyData.id);
107
+ const mimeType = isValidString(legacyData.mimeType);
108
+ const dataSource = (_b = legacyData.data) !== null && _b !== void 0 ? _b : legacyData.dataURL;
109
+ const data = isValidUint8Array(dataSource);
110
+ if (!id || !mimeType || !data)
111
+ continue;
112
+ const revId = `${id}_rev1`;
113
+ const created = isFiniteNumber(legacyData.created) ? legacyData.created : Date.now();
114
+ restoredFiles[id] = {
115
+ id,
116
+ activeRevisionId: revId,
117
+ updated: created,
118
+ version: isFiniteNumber(legacyData.version) ? legacyData.version : undefined,
119
+ revisions: {
120
+ [revId]: {
121
+ id: revId,
122
+ sizeBytes: data.byteLength,
123
+ mimeType,
124
+ created,
125
+ lastRetrieved: isFiniteNumber(legacyData.lastRetrieved)
126
+ ? legacyData.lastRetrieved
127
+ : undefined,
128
+ data,
129
+ },
130
+ },
131
+ };
84
132
  }
85
133
  return restoredFiles;
86
134
  };
@@ -79,30 +79,36 @@ export type DucUcs = {
79
79
  angle: Radian;
80
80
  };
81
81
  export type Scope = SupportedMeasures;
82
- export type DucExternalFileData = {
82
+ export type ExternalFileRevision = {
83
+ id: string;
84
+ sizeBytes: number;
85
+ /** Content hash for integrity checks and optional deduplication. */
86
+ checksum?: string;
87
+ /** Original upload filename shown to the user. */
88
+ sourceName?: string;
83
89
  mimeType: string;
84
- id: ExternalFileId;
85
- data: Uint8Array;
86
- /**
87
- * Epoch timestamp in milliseconds
88
- */
90
+ /** Optional note describing what changed in this revision. */
91
+ message?: string;
92
+ /** Epoch timestamp in milliseconds when this revision was created. */
89
93
  created: number;
90
94
  /**
91
- * Indicates when the file was last retrieved from storage to be loaded
92
- * onto the scene. We use this flag to determine whether to delete unused
93
- * files from storage.
94
- *
95
- * Epoch timestamp in milliseconds.
95
+ * Epoch timestamp in milliseconds when this revision was last loaded onto
96
+ * the scene. Used to determine whether to delete unused files from storage.
96
97
  */
97
98
  lastRetrieved?: number;
98
- /**
99
- * indicates the version of the file. This can be used to determine whether
100
- * the file data has changed e.g. as part of restore due to schema update.
101
- */
99
+ /** The actual file content bytes. */
100
+ data: Uint8Array;
101
+ };
102
+ export type DucExternalFile = {
103
+ id: ExternalFileId;
104
+ activeRevisionId: string;
105
+ /** Epoch ms when the logical file was last mutated (revision added or active changed). */
106
+ updated: number;
107
+ /** All revisions of this file, keyed by their id. */
108
+ revisions: Record<string, ExternalFileRevision>;
102
109
  version?: number;
103
110
  };
104
- export type DucExternalFileMetadata = Omit<DucExternalFileData, "data">;
105
- export type DucExternalFiles = Record<DucElement["id"], DucExternalFileData>;
111
+ export type DucExternalFiles = Record<ExternalFileId, DucExternalFile>;
106
112
  export type SuggestedBinding = NonDeleted<DucBindableElement> | SuggestedPointBinding;
107
113
  export type SuggestedPointBinding = [
108
114
  NonDeleted<DucLinearElement>,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ducjs",
3
- "version": "3.0.5",
3
+ "version": "3.1.0",
4
4
  "description": "The duc 2D CAD file format is a cornerstone of our advanced design system, conceived to cater to professionals seeking precision and efficiency in their design work.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",