@peers-app/peers-sdk 0.27.3 → 0.27.4

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 (258) hide show
  1. package/dist/contracts/contract-proxy.d.ts +7 -0
  2. package/dist/contracts/contract-proxy.js +19 -2
  3. package/dist/contracts/create-local-consumer.js +16 -1
  4. package/dist/contracts/index.d.ts +2 -0
  5. package/dist/contracts/index.js +2 -0
  6. package/dist/contracts/system/index.d.ts +2 -0
  7. package/dist/contracts/system/index.js +2 -0
  8. package/dist/contracts/system/invites-contract.js +2 -1
  9. package/dist/contracts/system/sync-check-contract.d.ts +97 -0
  10. package/dist/contracts/system/sync-check-contract.js +115 -0
  11. package/dist/contracts/tool-arguments.d.ts +15 -0
  12. package/dist/contracts/tool-arguments.js +41 -0
  13. package/dist/contracts/tool-catalog-service.js +2 -12
  14. package/dist/data/change-tracking.d.ts +40 -0
  15. package/dist/data/change-tracking.js +89 -0
  16. package/dist/data/orm/types.d.ts +24 -7
  17. package/dist/data/orm/types.js +97 -24
  18. package/dist/define/manifest.js +6 -34
  19. package/dist/package-loader/package-loader.js +27 -22
  20. package/dist/system-ids.d.ts +5 -0
  21. package/dist/system-ids.js +6 -1
  22. package/package.json +4 -2
  23. package/dist/context/user-context-identity.test.d.ts +0 -1
  24. package/dist/context/user-context-identity.test.js +0 -220
  25. package/dist/contracts/__tests__/action-dispatch.test.d.ts +0 -1
  26. package/dist/contracts/__tests__/action-dispatch.test.js +0 -319
  27. package/dist/contracts/__tests__/also-implements.test.d.ts +0 -1
  28. package/dist/contracts/__tests__/also-implements.test.js +0 -110
  29. package/dist/contracts/__tests__/builder.test.d.ts +0 -1
  30. package/dist/contracts/__tests__/builder.test.js +0 -669
  31. package/dist/contracts/__tests__/contract-events.test.d.ts +0 -1
  32. package/dist/contracts/__tests__/contract-events.test.js +0 -199
  33. package/dist/contracts/__tests__/contract-observables.test.d.ts +0 -1
  34. package/dist/contracts/__tests__/contract-observables.test.js +0 -290
  35. package/dist/contracts/__tests__/contract-provider-router.test.d.ts +0 -1
  36. package/dist/contracts/__tests__/contract-provider-router.test.js +0 -315
  37. package/dist/contracts/__tests__/contract-proxy.test.d.ts +0 -1
  38. package/dist/contracts/__tests__/contract-proxy.test.js +0 -657
  39. package/dist/contracts/__tests__/contract-resolvers.test.d.ts +0 -1
  40. package/dist/contracts/__tests__/contract-resolvers.test.js +0 -758
  41. package/dist/contracts/__tests__/contract-subscription-lifecycle.test.d.ts +0 -1
  42. package/dist/contracts/__tests__/contract-subscription-lifecycle.test.js +0 -353
  43. package/dist/contracts/__tests__/contract-table-events.test.d.ts +0 -1
  44. package/dist/contracts/__tests__/contract-table-events.test.js +0 -209
  45. package/dist/contracts/__tests__/contract-transport.test.d.ts +0 -1
  46. package/dist/contracts/__tests__/contract-transport.test.js +0 -163
  47. package/dist/contracts/__tests__/extract.test.d.ts +0 -1
  48. package/dist/contracts/__tests__/extract.test.js +0 -145
  49. package/dist/contracts/__tests__/frozen-contracts.test.d.ts +0 -1
  50. package/dist/contracts/__tests__/frozen-contracts.test.js +0 -191
  51. package/dist/contracts/__tests__/generic-tool-dispatch.test.d.ts +0 -1
  52. package/dist/contracts/__tests__/generic-tool-dispatch.test.js +0 -130
  53. package/dist/contracts/__tests__/host-contract-client.test.d.ts +0 -1
  54. package/dist/contracts/__tests__/host-contract-client.test.js +0 -793
  55. package/dist/contracts/__tests__/host-contract-consumer.test.d.ts +0 -1
  56. package/dist/contracts/__tests__/host-contract-consumer.test.js +0 -163
  57. package/dist/contracts/__tests__/host-contract-transport.test.d.ts +0 -1
  58. package/dist/contracts/__tests__/host-contract-transport.test.js +0 -66
  59. package/dist/contracts/__tests__/integration.test.d.ts +0 -1
  60. package/dist/contracts/__tests__/integration.test.js +0 -348
  61. package/dist/contracts/__tests__/legacy-removal.test.d.ts +0 -1
  62. package/dist/contracts/__tests__/legacy-removal.test.js +0 -128
  63. package/dist/contracts/__tests__/persistent-registry.test.d.ts +0 -1
  64. package/dist/contracts/__tests__/persistent-registry.test.js +0 -109
  65. package/dist/contracts/__tests__/playground.test.d.ts +0 -1
  66. package/dist/contracts/__tests__/playground.test.js +0 -216
  67. package/dist/contracts/__tests__/registry.test.d.ts +0 -1
  68. package/dist/contracts/__tests__/registry.test.js +0 -238
  69. package/dist/contracts/__tests__/remote-contract-transport.test.d.ts +0 -1
  70. package/dist/contracts/__tests__/remote-contract-transport.test.js +0 -119
  71. package/dist/contracts/__tests__/system-contracts.test.d.ts +0 -1
  72. package/dist/contracts/__tests__/system-contracts.test.js +0 -247
  73. package/dist/contracts/__tests__/target-dispatch.d.ts +0 -11
  74. package/dist/contracts/__tests__/target-dispatch.js +0 -24
  75. package/dist/contracts/__tests__/tool-catalog-inventory.test.d.ts +0 -1
  76. package/dist/contracts/__tests__/tool-catalog-inventory.test.js +0 -44
  77. package/dist/contracts/__tests__/tool-catalog-service.test.d.ts +0 -1
  78. package/dist/contracts/__tests__/tool-catalog-service.test.js +0 -482
  79. package/dist/contracts/__tests__/tool-catalog.test.d.ts +0 -1
  80. package/dist/contracts/__tests__/tool-catalog.test.js +0 -1199
  81. package/dist/contracts/__tests__/validate.test.d.ts +0 -1
  82. package/dist/contracts/__tests__/validate.test.js +0 -991
  83. package/dist/contracts/__tests__/workflow-context.test.d.ts +0 -1
  84. package/dist/contracts/__tests__/workflow-context.test.js +0 -50
  85. package/dist/data/access-level.test.d.ts +0 -1
  86. package/dist/data/access-level.test.js +0 -53
  87. package/dist/data/change-tracking.test.d.ts +0 -1
  88. package/dist/data/change-tracking.test.js +0 -62
  89. package/dist/data/files/files.test.d.ts +0 -1
  90. package/dist/data/files/files.test.js +0 -770
  91. package/dist/data/group-members.test.d.ts +0 -1
  92. package/dist/data/group-members.test.js +0 -336
  93. package/dist/data/group-share.test.d.ts +0 -1
  94. package/dist/data/group-share.test.js +0 -110
  95. package/dist/data/groups.test.d.ts +0 -1
  96. package/dist/data/groups.test.js +0 -153
  97. package/dist/data/orm/cursor.test.d.ts +0 -1
  98. package/dist/data/orm/cursor.test.js +0 -314
  99. package/dist/data/orm/data-query.mongo.test.d.ts +0 -1
  100. package/dist/data/orm/data-query.mongo.test.js +0 -367
  101. package/dist/data/orm/data-query.sqlite.test.d.ts +0 -1
  102. package/dist/data/orm/data-query.sqlite.test.js +0 -365
  103. package/dist/data/orm/data-query.test.d.ts +0 -1
  104. package/dist/data/orm/data-query.test.js +0 -545
  105. package/dist/data/orm/dependency-injection.test.d.ts +0 -1
  106. package/dist/data/orm/dependency-injection.test.js +0 -171
  107. package/dist/data/orm/event-registry.test.d.ts +0 -1
  108. package/dist/data/orm/event-registry.test.js +0 -44
  109. package/dist/data/orm/multi-cursors.test.d.ts +0 -1
  110. package/dist/data/orm/multi-cursors.test.js +0 -463
  111. package/dist/data/orm/sql.data-source.test.d.ts +0 -1
  112. package/dist/data/orm/sql.data-source.test.js +0 -731
  113. package/dist/data/orm/table-container-events.test.d.ts +0 -1
  114. package/dist/data/orm/table-container-events.test.js +0 -101
  115. package/dist/data/orm/table-container.test.d.ts +0 -1
  116. package/dist/data/orm/table-container.test.js +0 -273
  117. package/dist/data/orm/table.event-source.test.d.ts +0 -1
  118. package/dist/data/orm/table.event-source.test.js +0 -331
  119. package/dist/data/orm/types.test.d.ts +0 -1
  120. package/dist/data/orm/types.test.js +0 -409
  121. package/dist/data/package-table-identity.test.d.ts +0 -1
  122. package/dist/data/package-table-identity.test.js +0 -46
  123. package/dist/data/package-version-permissions.test.d.ts +0 -1
  124. package/dist/data/package-version-permissions.test.js +0 -107
  125. package/dist/data/package-version-resolver.test.d.ts +0 -4
  126. package/dist/data/package-version-resolver.test.js +0 -543
  127. package/dist/data/package-versions.test.d.ts +0 -1
  128. package/dist/data/package-versions.test.js +0 -227
  129. package/dist/data/persistent-vars.conflict.test.d.ts +0 -1
  130. package/dist/data/persistent-vars.conflict.test.js +0 -126
  131. package/dist/data/persistent-vars.migration.test.d.ts +0 -1
  132. package/dist/data/persistent-vars.migration.test.js +0 -133
  133. package/dist/data/persistent-vars.package-scope.test.d.ts +0 -1
  134. package/dist/data/persistent-vars.package-scope.test.js +0 -27
  135. package/dist/data/sync-recovery-queue.test.d.ts +0 -1
  136. package/dist/data/sync-recovery-queue.test.js +0 -215
  137. package/dist/data/table-signing.test.d.ts +0 -1
  138. package/dist/data/table-signing.test.js +0 -118
  139. package/dist/data/tool-access.test.d.ts +0 -1
  140. package/dist/data/tool-access.test.js +0 -166
  141. package/dist/data/user-permissions.test.d.ts +0 -1
  142. package/dist/data/user-permissions.test.js +0 -305
  143. package/dist/data/user-trust-levels.test.d.ts +0 -1
  144. package/dist/data/user-trust-levels.test.js +0 -37
  145. package/dist/define/also-implements.test.d.ts +0 -1
  146. package/dist/define/also-implements.test.js +0 -123
  147. package/dist/define/declare-contract.test.d.ts +0 -1
  148. package/dist/define/declare-contract.test.js +0 -573
  149. package/dist/define/define.test.d.ts +0 -1
  150. package/dist/define/define.test.js +0 -1010
  151. package/dist/define/empty-fields.test.d.ts +0 -1
  152. package/dist/define/empty-fields.test.js +0 -55
  153. package/dist/define/events-observables.test.d.ts +0 -1
  154. package/dist/define/events-observables.test.js +0 -1013
  155. package/dist/define/serve-members.test.d.ts +0 -1
  156. package/dist/define/serve-members.test.js +0 -630
  157. package/dist/define/vars.test.d.ts +0 -1
  158. package/dist/define/vars.test.js +0 -39
  159. package/dist/device/binary-peer-connection-v2.test.d.ts +0 -1
  160. package/dist/device/binary-peer-connection-v2.test.js +0 -730
  161. package/dist/device/binary-peer-connection.test.d.ts +0 -1
  162. package/dist/device/binary-peer-connection.test.js +0 -184
  163. package/dist/device/connection.test.d.ts +0 -1
  164. package/dist/device/connection.test.js +0 -613
  165. package/dist/device/device-election.test.d.ts +0 -1
  166. package/dist/device/device-election.test.js +0 -516
  167. package/dist/device/device.test.d.ts +0 -1
  168. package/dist/device/device.test.js +0 -243
  169. package/dist/device/frame-cipher.test.d.ts +0 -1
  170. package/dist/device/frame-cipher.test.js +0 -219
  171. package/dist/device/get-trust-level-fn.test.d.ts +0 -1
  172. package/dist/device/get-trust-level-fn.test.js +0 -685
  173. package/dist/device/streamed-socket.test.d.ts +0 -1
  174. package/dist/device/streamed-socket.test.js +0 -216
  175. package/dist/device/tx-encoding.test.d.ts +0 -1
  176. package/dist/device/tx-encoding.test.js +0 -295
  177. package/dist/device-pairing/cpace.test.d.ts +0 -1
  178. package/dist/device-pairing/cpace.test.js +0 -116
  179. package/dist/device-pairing/device-pairing.test.d.ts +0 -1
  180. package/dist/device-pairing/device-pairing.test.js +0 -211
  181. package/dist/hlc.test.d.ts +0 -1
  182. package/dist/hlc.test.js +0 -45
  183. package/dist/host-credentials.test.d.ts +0 -1
  184. package/dist/host-credentials.test.js +0 -88
  185. package/dist/identity/account-email.test.d.ts +0 -1
  186. package/dist/identity/account-email.test.js +0 -59
  187. package/dist/identity/anchor-document.test.d.ts +0 -1
  188. package/dist/identity/anchor-document.test.js +0 -16
  189. package/dist/identity/anchor-resolution.test.d.ts +0 -1
  190. package/dist/identity/anchor-resolution.test.js +0 -130
  191. package/dist/identity/key-change.test.d.ts +0 -1
  192. package/dist/identity/key-change.test.js +0 -811
  193. package/dist/invites/group-approval.test.d.ts +0 -1
  194. package/dist/invites/group-approval.test.js +0 -223
  195. package/dist/invites/invite-token.test.d.ts +0 -1
  196. package/dist/invites/invite-token.test.js +0 -123
  197. package/dist/keys.test.d.ts +0 -1
  198. package/dist/keys.test.js +0 -336
  199. package/dist/logging/console-logger.test.d.ts +0 -1
  200. package/dist/logging/console-logger.test.js +0 -300
  201. package/dist/logging/console-logs.table.test.d.ts +0 -1
  202. package/dist/logging/console-logs.table.test.js +0 -156
  203. package/dist/observable.test.d.ts +0 -1
  204. package/dist/observable.test.js +0 -309
  205. package/dist/package-build/build-package.test.d.ts +0 -1
  206. package/dist/package-build/build-package.test.js +0 -204
  207. package/dist/package-build/contract-add.test.d.ts +0 -1
  208. package/dist/package-build/contract-add.test.js +0 -467
  209. package/dist/package-build/freeze-contracts.test.d.ts +0 -1
  210. package/dist/package-build/freeze-contracts.test.js +0 -355
  211. package/dist/package-installer/package-author-signing.test.d.ts +0 -1
  212. package/dist/package-installer/package-author-signing.test.js +0 -200
  213. package/dist/package-installer/package-cloner.test.d.ts +0 -1
  214. package/dist/package-installer/package-cloner.test.js +0 -278
  215. package/dist/package-installer/package-creator.test.d.ts +0 -1
  216. package/dist/package-installer/package-creator.test.js +0 -908
  217. package/dist/package-installer/package-dev-tools.test.d.ts +0 -1
  218. package/dist/package-installer/package-dev-tools.test.js +0 -64
  219. package/dist/package-installer/package-installer.test.d.ts +0 -1
  220. package/dist/package-installer/package-installer.test.js +0 -882
  221. package/dist/package-installer/package-propagation.test.d.ts +0 -1
  222. package/dist/package-installer/package-propagation.test.js +0 -1269
  223. package/dist/package-installer/package-publisher.test.d.ts +0 -1
  224. package/dist/package-installer/package-publisher.test.js +0 -153
  225. package/dist/package-installer/package-remote-checker.test.d.ts +0 -1
  226. package/dist/package-installer/package-remote-checker.test.js +0 -334
  227. package/dist/package-installer/package-seed-installer.test.d.ts +0 -1
  228. package/dist/package-installer/package-seed-installer.test.js +0 -123
  229. package/dist/package-installer/package-tarball.test.d.ts +0 -1
  230. package/dist/package-installer/package-tarball.test.js +0 -75
  231. package/dist/package-installer/peers-core-installer.test.d.ts +0 -1
  232. package/dist/package-installer/peers-core-installer.test.js +0 -49
  233. package/dist/package-installer/peers-core-trust-anchor.test.d.ts +0 -1
  234. package/dist/package-installer/peers-core-trust-anchor.test.js +0 -155
  235. package/dist/package-loader/contract-package-loader.test.d.ts +0 -1
  236. package/dist/package-loader/contract-package-loader.test.js +0 -274
  237. package/dist/package-loader/host-authority-loading.test.d.ts +0 -1
  238. package/dist/package-loader/host-authority-loading.test.js +0 -104
  239. package/dist/package-loader/host-dispatch-policy.test.d.ts +0 -1
  240. package/dist/package-loader/host-dispatch-policy.test.js +0 -56
  241. package/dist/package-loader/isolated-artifact.test.d.ts +0 -1
  242. package/dist/package-loader/isolated-artifact.test.js +0 -688
  243. package/dist/package-loader/isolated-persistent-var-gateway.test.d.ts +0 -1
  244. package/dist/package-loader/isolated-persistent-var-gateway.test.js +0 -56
  245. package/dist/package-loader/isolated-table-gateway.test.d.ts +0 -1
  246. package/dist/package-loader/isolated-table-gateway.test.js +0 -633
  247. package/dist/package-loader/package-loader.test.d.ts +0 -1
  248. package/dist/package-loader/package-loader.test.js +0 -1764
  249. package/dist/package-loader/resolve-consumed-contracts.test.d.ts +0 -1
  250. package/dist/package-loader/resolve-consumed-contracts.test.js +0 -166
  251. package/dist/serial-json.compat.test.d.ts +0 -1
  252. package/dist/serial-json.compat.test.js +0 -369
  253. package/dist/serial-json.test.d.ts +0 -1
  254. package/dist/serial-json.test.js +0 -401
  255. package/dist/testing/mock-host-contract.test.d.ts +0 -1
  256. package/dist/testing/mock-host-contract.test.js +0 -782
  257. package/dist/utils.test.d.ts +0 -1
  258. package/dist/utils.test.js +0 -232
@@ -1,770 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- const keys_1 = require("../../keys");
4
- const field_type_1 = require("../../types/field-type");
5
- const utils_1 = require("../../utils");
6
- const file_types_1 = require("./file.types");
7
- const files_1 = require("./files");
8
- // Mock data source for testing
9
- class MockDataSource {
10
- tableName;
11
- primaryKeyName;
12
- data = new Map();
13
- constructor(tableName, primaryKeyName = "fileId", initialData = []) {
14
- this.tableName = tableName;
15
- this.primaryKeyName = primaryKeyName;
16
- initialData.forEach((item) => {
17
- this.data.set(item[primaryKeyName], item);
18
- });
19
- }
20
- async get(id) {
21
- return this.data.get(id);
22
- }
23
- async list() {
24
- return Array.from(this.data.values());
25
- }
26
- async save(record) {
27
- this.data.set(record[this.primaryKeyName], record);
28
- return record;
29
- }
30
- async insert(record) {
31
- this.data.set(record[this.primaryKeyName], record);
32
- return record;
33
- }
34
- async update(record) {
35
- this.data.set(record[this.primaryKeyName], record);
36
- return record;
37
- }
38
- async delete(idOrRecord) {
39
- const id = typeof idOrRecord === "string" ? idOrRecord : idOrRecord[this.primaryKeyName];
40
- this.data.delete(id);
41
- }
42
- async count() {
43
- return this.data.size;
44
- }
45
- }
46
- // Mock FileOps for testing
47
- class MockFileOps {
48
- files = new Map();
49
- async downloadFileChunk(chunkHash) {
50
- // For testing, just return the chunk if it exists locally
51
- const chunkPath = `file_chunks/${chunkHash}`;
52
- const data = this.files.get(chunkPath);
53
- if (!data) {
54
- throw new Error(`Chunk not found: ${chunkPath}`);
55
- }
56
- return data;
57
- }
58
- async fileExists(path) {
59
- return this.files.has(path);
60
- }
61
- async readFile(path) {
62
- const data = this.files.get(path);
63
- if (!data) {
64
- throw new Error(`File not found: ${path}`);
65
- }
66
- return data;
67
- }
68
- async writeFile(path, data) {
69
- this.files.set(path, data);
70
- }
71
- async deletePath(path) {
72
- this.files.delete(path);
73
- }
74
- // Helper method for testing
75
- getStoredFiles() {
76
- return Array.from(this.files.keys());
77
- }
78
- }
79
- describe("FileTable", () => {
80
- let fileTable;
81
- let mockFileOps;
82
- beforeEach(() => {
83
- // Create mock data source
84
- const mockDataSource = new MockDataSource("Files", "fileId");
85
- // Use direct instantiation since factory requires setup
86
- const metaData = {
87
- name: "Files",
88
- description: "Files stored in the chunked file system for peer sharing",
89
- primaryKeyName: "fileId",
90
- fields: [
91
- { name: "fileId", type: field_type_1.FieldType.string },
92
- { name: "name", type: field_type_1.FieldType.string },
93
- { name: "fileSize", type: field_type_1.FieldType.number },
94
- { name: "fileHash", type: field_type_1.FieldType.string },
95
- { name: "mimeType", type: field_type_1.FieldType.string, optional: true },
96
- { name: "chunkHashes", type: field_type_1.FieldType.string, isArray: true, optional: true },
97
- { name: "isIndexFile", type: field_type_1.FieldType.boolean, optional: true },
98
- { name: "indexFileId", type: field_type_1.FieldType.string, optional: true },
99
- ],
100
- };
101
- // Create mock data context
102
- const mockDataContextForRegistry = {
103
- dataContextId: "files-test",
104
- };
105
- const { EventRegistry } = require("../orm/event-registry");
106
- const eventRegistry = new EventRegistry(mockDataContextForRegistry);
107
- const _mockDataContext = {
108
- dataSourceFactory: () => mockDataSource,
109
- eventRegistry: eventRegistry,
110
- };
111
- // Create FileTable instance directly with mock dataSource
112
- const deps = {
113
- dataSource: mockDataSource,
114
- eventRegistry,
115
- schema: file_types_1.fileSchema,
116
- };
117
- fileTable = new files_1.FilesTable(metaData, deps);
118
- // Set up mock file operations
119
- mockFileOps = new MockFileOps();
120
- (0, file_types_1.setFileOps)(mockFileOps);
121
- });
122
- afterEach(() => {
123
- // Clean up global state to prevent Jest from hanging
124
- (0, file_types_1.resetFileOps)();
125
- });
126
- describe("saveFile", () => {
127
- it("should save a small file in a single chunk", async () => {
128
- const fileId = (0, utils_1.newid)();
129
- const data = new Uint8Array(Buffer.from("Hello, World!", "utf8"));
130
- const metadata = {
131
- fileId,
132
- name: "test.txt",
133
- fileSize: data.length,
134
- mimeType: "text/plain",
135
- };
136
- const result = await fileTable.saveFile(metadata, data);
137
- expect(result.fileId).toBe(fileId);
138
- expect(result.chunkHashes).toHaveLength(1);
139
- // Check that chunk was written by hash
140
- const chunkHash = result.chunkHashes?.[0];
141
- const chunkPath = `file_chunks/${chunkHash}`;
142
- expect(await mockFileOps.fileExists(chunkPath)).toBe(true);
143
- const storedChunk = await mockFileOps.readFile(chunkPath);
144
- expect(storedChunk).toBeInstanceOf(Uint8Array);
145
- expect(storedChunk).toEqual(data);
146
- });
147
- it("should save a large file in multiple chunks", async () => {
148
- const fileId = (0, utils_1.newid)();
149
- const largeData = new Uint8Array(Buffer.alloc(file_types_1.FILE_CHUNK_SIZE + 1000, "A")); // Slightly larger than one chunk
150
- const metadata = {
151
- fileId,
152
- name: "large.txt",
153
- fileSize: largeData.length,
154
- mimeType: "text/plain",
155
- };
156
- const result = await fileTable.saveFile(metadata, largeData);
157
- expect(result.chunkHashes).toHaveLength(2);
158
- // Check that both chunks were written by hash
159
- const chunk0Hash = result.chunkHashes?.[0];
160
- const chunk1Hash = result.chunkHashes?.[1];
161
- expect(await mockFileOps.fileExists(`file_chunks/${chunk0Hash}`)).toBe(true);
162
- expect(await mockFileOps.fileExists(`file_chunks/${chunk1Hash}`)).toBe(true);
163
- // Verify chunk sizes
164
- const chunk0 = await mockFileOps.readFile(`file_chunks/${chunk0Hash}`);
165
- const chunk1 = await mockFileOps.readFile(`file_chunks/${chunk1Hash}`);
166
- expect(chunk0.length).toBe(file_types_1.FILE_CHUNK_SIZE);
167
- expect(chunk1.length).toBe(1000);
168
- });
169
- });
170
- describe("getFile", () => {
171
- it("should retrieve a single-chunk file", async () => {
172
- const fileId = (0, utils_1.newid)();
173
- const originalData = new Uint8Array(Buffer.from("Test content", "utf8"));
174
- const metadata = {
175
- fileId,
176
- name: "test.txt",
177
- fileSize: originalData.length,
178
- mimeType: "text/plain",
179
- };
180
- // Save file first
181
- await fileTable.saveFile(metadata, originalData);
182
- // Retrieve file
183
- const retrievedData = await fileTable.getFileContents(fileId);
184
- expect(new Uint8Array(retrievedData)).toEqual(originalData);
185
- });
186
- it("should retrieve a multi-chunk file", async () => {
187
- const fileId = (0, utils_1.newid)();
188
- const originalData = new Uint8Array(Buffer.alloc(file_types_1.FILE_CHUNK_SIZE + 500, "B"));
189
- const metadata = {
190
- fileId,
191
- name: "large.txt",
192
- fileSize: originalData.length,
193
- mimeType: "text/plain",
194
- };
195
- // Save file first
196
- await fileTable.saveFile(metadata, originalData);
197
- // Retrieve file
198
- const retrievedData = await fileTable.getFileContents(fileId);
199
- expect(new Uint8Array(retrievedData)).toEqual(originalData);
200
- });
201
- it("should return null for non-existent file", async () => {
202
- const result = await fileTable.getFileContents("non-existent");
203
- expect(result).toBeNull();
204
- });
205
- it("should throw integrity error when fileHash doesn't match chunk hashes", async () => {
206
- const fileId = (0, utils_1.newid)();
207
- const metadata = {
208
- fileId,
209
- name: "test.txt",
210
- fileSize: 100,
211
- fileHash: "hash123",
212
- chunkHashes: ["hash1", "hash2"],
213
- };
214
- // Insert metadata directly with mismatched fileHash
215
- await fileTable.dataSource.insert(metadata);
216
- await expect(fileTable.getFileContents(fileId)).rejects.toThrow("File integrity check failed");
217
- });
218
- it("should return null when chunk is missing during read", async () => {
219
- const result = await fileTable.getFileContents("non-existent");
220
- expect(result).toBeNull();
221
- });
222
- });
223
- describe("deleteFile", () => {
224
- it("should delete file from database but preserve chunks", async () => {
225
- const fileId = (0, utils_1.newid)();
226
- const data = new Uint8Array(Buffer.from("Delete me", "utf8"));
227
- const metadata = {
228
- fileId,
229
- name: "delete.txt",
230
- fileSize: data.length,
231
- mimeType: "text/plain",
232
- };
233
- // Save file first
234
- const saved = await fileTable.saveFile(metadata, data);
235
- // Verify file exists
236
- const fileData = await fileTable.getFileContents(fileId);
237
- expect(new Uint8Array(fileData)).toEqual(data);
238
- const chunkHash = saved.chunkHashes?.[0];
239
- expect(await mockFileOps.fileExists(`file_chunks/${chunkHash}`)).toBe(true);
240
- // Delete file
241
- await fileTable.deleteFile(fileId);
242
- // Verify file metadata is gone but chunk remains (for potential sharing)
243
- expect(await fileTable.getFileContents(fileId)).toBeNull();
244
- expect(await mockFileOps.fileExists(`file_chunks/${chunkHash}`)).toBe(true);
245
- });
246
- it("should handle deleting non-existent file gracefully", async () => {
247
- // Should not throw
248
- await fileTable.deleteFile("non-existent");
249
- });
250
- it("should delete multi-chunk file from database but preserve chunks", async () => {
251
- const fileId = (0, utils_1.newid)();
252
- const data = new Uint8Array(Buffer.alloc(file_types_1.FILE_CHUNK_SIZE + 100, "C"));
253
- const metadata = {
254
- fileId,
255
- name: "multi.txt",
256
- fileSize: data.length,
257
- mimeType: "text/plain",
258
- };
259
- // Save file first
260
- const saved = await fileTable.saveFile(metadata, data);
261
- // Verify chunks exist by hash
262
- const chunk0Hash = saved.chunkHashes?.[0];
263
- const chunk1Hash = saved.chunkHashes?.[1];
264
- expect(await mockFileOps.fileExists(`file_chunks/${chunk0Hash}`)).toBe(true);
265
- expect(await mockFileOps.fileExists(`file_chunks/${chunk1Hash}`)).toBe(true);
266
- // Delete file
267
- await fileTable.deleteFile(fileId);
268
- // Verify file metadata is gone but chunks remain (for potential sharing)
269
- expect(await fileTable.getFileContents(fileId)).toBeNull();
270
- expect(await mockFileOps.fileExists(`file_chunks/${chunk0Hash}`)).toBe(true);
271
- expect(await mockFileOps.fileExists(`file_chunks/${chunk1Hash}`)).toBe(true);
272
- });
273
- });
274
- describe("chunk deduplication", () => {
275
- it("should deduplicate identical chunks across different files", async () => {
276
- const fileId1 = (0, utils_1.newid)();
277
- const fileId2 = (0, utils_1.newid)();
278
- // Create two files with identical content
279
- const data = new Uint8Array(Buffer.from("Identical content for deduplication test", "utf8"));
280
- const metadata1 = {
281
- fileId: fileId1,
282
- name: "file1.txt",
283
- fileSize: data.length,
284
- mimeType: "text/plain",
285
- };
286
- const metadata2 = {
287
- fileId: fileId2,
288
- name: "file2.txt",
289
- fileSize: data.length,
290
- mimeType: "text/plain",
291
- };
292
- // Save both files
293
- const saved1 = await fileTable.saveFile(metadata1, data);
294
- const saved2 = await fileTable.saveFile(metadata2, data);
295
- // Both files should have the same chunk hash (deduplication)
296
- expect(saved1.chunkHashes).toEqual(saved2.chunkHashes);
297
- // Only one chunk should be stored physically
298
- expect(saved1.chunkHashes?.length).toBeGreaterThan(0);
299
- const chunkHash = saved1.chunkHashes[0];
300
- const chunkPath = `file_chunks/${chunkHash}`;
301
- expect(await mockFileOps.fileExists(chunkPath)).toBe(true);
302
- expect(mockFileOps.getStoredFiles().filter((path) => path.includes(chunkHash))).toHaveLength(1);
303
- // Both files should retrieve the same content
304
- const file1Data = await fileTable.getFileContents(fileId1);
305
- const file2Data = await fileTable.getFileContents(fileId2);
306
- expect(new Uint8Array(file1Data)).toEqual(data);
307
- expect(new Uint8Array(file2Data)).toEqual(data);
308
- });
309
- });
310
- describe("Merkle tree (chunk index files)", () => {
311
- it("should use direct chunk hashes for files under threshold", async () => {
312
- const fileId = (0, utils_1.newid)();
313
- // Create a small file that's well under the 1000 chunk threshold
314
- const smallData = new Uint8Array(Buffer.from("Small file content for testing", "utf8"));
315
- const metadata = {
316
- fileId,
317
- name: "small.bin",
318
- fileSize: smallData.length,
319
- mimeType: "application/octet-stream",
320
- };
321
- // Save small file
322
- const result = await fileTable.saveFile(metadata, smallData);
323
- // Small file should use direct chunk hashes, not index file
324
- expect(result.chunkHashes).toBeTruthy();
325
- expect(result.chunkHashes).toHaveLength(1);
326
- expect(result.indexFileId).toBeUndefined();
327
- // Should be able to retrieve the file
328
- const retrievedData = await fileTable.getFileContents(fileId);
329
- expect(new Uint8Array(retrievedData)).toEqual(smallData);
330
- });
331
- it("should use recursive index files for very large files", async () => {
332
- // Temporarily lower the threshold to test recursive behavior with smaller data
333
- const originalThreshold = file_types_1.CHUNK_INDEX_THRESHOLD;
334
- (0, file_types_1.setChunkIndexThreshold)(3); // Use index file for files with >3 chunks
335
- try {
336
- const fileId = (0, utils_1.newid)();
337
- // Create a file with 5 chunks (exceeds threshold of 3)
338
- const largeData = new Uint8Array(Buffer.alloc(file_types_1.FILE_CHUNK_SIZE * 4 + 1000, "X")); // 4 full chunks + partial chunk = 5 chunks
339
- const metadata = {
340
- fileId,
341
- name: "large.bin",
342
- fileSize: largeData.length,
343
- mimeType: "application/octet-stream",
344
- };
345
- // Save large file - should create recursive index file
346
- const result = await fileTable.saveFile(metadata, largeData);
347
- // Large file should use index file, not direct chunk hashes
348
- expect(result.chunkHashes).toBeUndefined();
349
- expect(result.indexFileId).toBeTruthy();
350
- // Should be able to retrieve the file
351
- const retrievedData = await fileTable.getFileContents(fileId);
352
- expect(retrievedData).toBeTruthy();
353
- expect(new Uint8Array(retrievedData)).toEqual(largeData);
354
- // The index file should be marked as an index file
355
- const indexFile = await fileTable.dataSource.get(result.indexFileId);
356
- expect(indexFile).toBeTruthy();
357
- expect(indexFile?.isIndexFile).toBe(true);
358
- // The index file should contain JSON data
359
- expect(indexFile?.mimeType).toBe("application/json");
360
- expect(indexFile?.name).toMatch(/^index-.*\.json$/);
361
- }
362
- finally {
363
- // Restore original threshold
364
- (0, file_types_1.setChunkIndexThreshold)(originalThreshold);
365
- }
366
- });
367
- it("should handle missing index file gracefully", async () => {
368
- const fileId = (0, utils_1.newid)();
369
- const metadata = {
370
- fileId,
371
- name: "test.txt",
372
- fileSize: 100,
373
- fileHash: "hash123",
374
- indexFileId: "missing_index_file_id",
375
- };
376
- // Insert metadata directly without creating index file
377
- await fileTable.dataSource.insert(metadata);
378
- await expect(fileTable.getFileContents(fileId)).rejects.toThrow("Index file not found: missing_index_file_id");
379
- });
380
- });
381
- describe("Streaming API", () => {
382
- describe("FileWriteStream", () => {
383
- it("should create a write stream and write small file chunk by chunk", async () => {
384
- const fileId = (0, utils_1.newid)();
385
- const metadata = {
386
- fileId,
387
- name: "stream-test.txt",
388
- fileSize: 0, // Will be calculated
389
- mimeType: "text/plain",
390
- };
391
- const writeStream = await fileTable.createWriteStream(metadata);
392
- // Write data in chunks
393
- await writeStream.write(new Uint8Array(Buffer.from("Hello, ", "utf8")));
394
- await writeStream.write(new Uint8Array(Buffer.from("streaming ", "utf8")));
395
- await writeStream.write(new Uint8Array(Buffer.from("world!", "utf8")));
396
- // Finalize the stream
397
- const result = await writeStream.finalize();
398
- expect(result.fileId).toBe(fileId);
399
- expect(result.name).toBe("stream-test.txt");
400
- expect(result.fileSize).toBe(23); // Total bytes written: 'Hello, streaming world!'
401
- expect(result.chunkHashes).toHaveLength(1); // Small file, single chunk
402
- // Verify we can read the file back
403
- const retrievedData = await fileTable.getFileContents(fileId);
404
- expect(Buffer.from(retrievedData).toString("utf8")).toBe("Hello, streaming world!");
405
- });
406
- it("should handle large file streaming across multiple chunks", async () => {
407
- const fileId = (0, utils_1.newid)();
408
- const metadata = {
409
- fileId,
410
- name: "large-stream.bin",
411
- fileSize: 0,
412
- mimeType: "application/octet-stream",
413
- };
414
- const writeStream = await fileTable.createWriteStream(metadata);
415
- // Write data larger than one chunk
416
- const chunkSize = file_types_1.FILE_CHUNK_SIZE;
417
- const chunk1 = new Uint8Array(Buffer.alloc(chunkSize, "A"));
418
- const chunk2 = new Uint8Array(Buffer.alloc(500, "B")); // Partial second chunk
419
- await writeStream.write(chunk1);
420
- await writeStream.write(chunk2);
421
- const result = await writeStream.finalize();
422
- expect(result.fileSize).toBe(chunkSize + 500);
423
- expect(result.chunkHashes).toHaveLength(2); // Two chunks
424
- // Verify content
425
- const retrievedData = await fileTable.getFileContents(fileId);
426
- expect(retrievedData?.length).toBe(chunkSize + 500);
427
- expect(new Uint8Array(retrievedData).subarray(0, chunkSize).every((b) => b === 65)).toBe(true); // All 'A's
428
- expect(new Uint8Array(retrievedData).subarray(chunkSize).every((b) => b === 66)).toBe(true); // All 'B's
429
- });
430
- it("should support aborting a write stream", async () => {
431
- const fileId = (0, utils_1.newid)();
432
- const metadata = {
433
- fileId,
434
- name: "aborted.txt",
435
- fileSize: 0,
436
- mimeType: "text/plain",
437
- };
438
- const writeStream = await fileTable.createWriteStream(metadata);
439
- await writeStream.write(new Uint8Array(Buffer.from("Some data", "utf8")));
440
- expect(writeStream.getBytesWritten()).toBe(9);
441
- // Abort the stream
442
- await writeStream.abort();
443
- expect(writeStream.isAborted()).toBe(true);
444
- // Should not be able to write after abort
445
- await expect(writeStream.write(new Uint8Array(Buffer.from("More data", "utf8")))).rejects.toThrow("Cannot write to aborted stream");
446
- // Should not be able to finalize after abort
447
- await expect(writeStream.finalize()).rejects.toThrow("Cannot finalize aborted stream");
448
- });
449
- it("should track progress during streaming", async () => {
450
- const fileId = (0, utils_1.newid)();
451
- const metadata = {
452
- fileId,
453
- name: "progress.txt",
454
- fileSize: 0,
455
- mimeType: "text/plain",
456
- };
457
- const writeStream = await fileTable.createWriteStream(metadata);
458
- expect(writeStream.getBytesWritten()).toBe(0);
459
- expect(writeStream.getChunkCount()).toBe(0);
460
- await writeStream.write(new Uint8Array(Buffer.from("First chunk", "utf8")));
461
- expect(writeStream.getBytesWritten()).toBe(11);
462
- expect(writeStream.getChunkCount()).toBe(0); // Not complete chunk yet
463
- await writeStream.write(new Uint8Array(Buffer.from(" Second part", "utf8")));
464
- expect(writeStream.getBytesWritten()).toBe(23);
465
- await writeStream.finalize();
466
- expect(writeStream.isFinalized()).toBe(true);
467
- });
468
- it("should handle finalize with multiple complete chunks in buffer", async () => {
469
- const fileId = (0, utils_1.newid)();
470
- const metadata = {
471
- fileId,
472
- name: "buffered-chunks.bin",
473
- fileSize: 0,
474
- mimeType: "application/octet-stream",
475
- };
476
- const writeStream = await fileTable.createWriteStream(metadata);
477
- // Write data that will create multiple complete chunks but don't trigger processCompleteChunks
478
- // This simulates the edge case where finalize() needs to process multiple chunks
479
- const chunkSize = file_types_1.FILE_CHUNK_SIZE;
480
- const buffer = Buffer.alloc(chunkSize * 2 + 500); // 2 full chunks + partial chunk
481
- buffer.fill(65); // Fill with 'A'
482
- const data = new Uint8Array(buffer);
483
- // Write as one large chunk to buffer (avoiding automatic processing)
484
- await writeStream.write(data);
485
- // At this point the buffer should have 2+ MB of data
486
- expect(writeStream.getBytesWritten()).toBe(data.length);
487
- // Finalize should process all complete chunks + final partial chunk
488
- const result = await writeStream.finalize();
489
- expect(result.fileSize).toBe(data.length);
490
- expect(result.chunkHashes).toHaveLength(3); // 2 complete + 1 partial chunk
491
- // Verify we can read it back correctly
492
- const retrievedData = await fileTable.getFileContents(fileId);
493
- expect(retrievedData?.length).toBe(data.length);
494
- expect(new Uint8Array(retrievedData)).toEqual(data);
495
- });
496
- });
497
- describe("FileReadStream", () => {
498
- it("should read file data in chunks using read stream", async () => {
499
- // First save a file using traditional method
500
- const fileId = (0, utils_1.newid)();
501
- const originalData = new Uint8Array(Buffer.from("This is test data for streaming read operations!", "utf8"));
502
- const metadata = {
503
- fileId,
504
- name: "read-test.txt",
505
- fileSize: originalData.length,
506
- mimeType: "text/plain",
507
- };
508
- await fileTable.saveFile(metadata, originalData);
509
- // Now open for streaming read
510
- const readStream = await fileTable.openReadStream(fileId);
511
- expect(readStream).toBeTruthy();
512
- // Read entire file
513
- const readData = await readStream?.readAll();
514
- expect(readData).not.toBeNull();
515
- expect(new Uint8Array(readData)).toEqual(originalData);
516
- // Check metadata
517
- const meta = readStream.getMetadata();
518
- expect(meta.fileId).toBe(fileId);
519
- expect(meta.name).toBe("read-test.txt");
520
- expect(meta.fileSize).toBe(originalData.length);
521
- });
522
- it("should support seeking within file", async () => {
523
- const fileId = (0, utils_1.newid)();
524
- const originalData = new Uint8Array(Buffer.from("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ", "utf8"));
525
- const metadata = {
526
- fileId,
527
- name: "seek-test.txt",
528
- fileSize: originalData.length,
529
- mimeType: "text/plain",
530
- };
531
- await fileTable.saveFile(metadata, originalData);
532
- const readStream = await fileTable.openReadStream(fileId);
533
- expect(readStream).toBeTruthy();
534
- // Seek to position 10 (should be at 'A')
535
- await readStream?.seek(10);
536
- expect(readStream?.getPosition()).toBe(10);
537
- // Read 5 bytes from position 10 (should get 'ABCDE')
538
- const chunk = await readStream?.read(5);
539
- expect(Buffer.from(chunk).toString("utf8")).toBe("ABCDE");
540
- expect(readStream?.getPosition()).toBe(15);
541
- // Seek back to beginning
542
- await readStream?.seek(0);
543
- expect(readStream?.getPosition()).toBe(0);
544
- const firstChunk = await readStream?.read(10);
545
- expect(Buffer.from(firstChunk).toString("utf8")).toBe("0123456789");
546
- expect(readStream?.getPosition()).toBe(10);
547
- // Seek to middle of data
548
- await readStream?.seek(20);
549
- expect(readStream?.getPosition()).toBe(20);
550
- const middleChunk = await readStream?.read(5);
551
- expect(Buffer.from(middleChunk).toString("utf8")).toBe("KLMNO");
552
- expect(readStream?.getPosition()).toBe(25);
553
- });
554
- it("should handle seeking across chunk boundaries in large files", async () => {
555
- // Create a file that spans multiple chunks
556
- const fileId = (0, utils_1.newid)();
557
- const chunkSize = file_types_1.FILE_CHUNK_SIZE;
558
- const data1 = Buffer.alloc(chunkSize, "A"); // First chunk: all A's
559
- const data2 = Buffer.alloc(chunkSize, "B"); // Second chunk: all B's
560
- const data3 = Buffer.alloc(500, "C"); // Third partial chunk: all C's
561
- const largeData = new Uint8Array(Buffer.concat([data1, data2, data3]));
562
- const metadata = {
563
- fileId,
564
- name: "large-seek-test.bin",
565
- fileSize: largeData.length,
566
- mimeType: "application/octet-stream",
567
- };
568
- await fileTable.saveFile(metadata, largeData);
569
- const readStream = await fileTable.openReadStream(fileId);
570
- expect(readStream).toBeTruthy();
571
- // Seek to end of first chunk
572
- await readStream?.seek(chunkSize - 5);
573
- const endOfFirst = await readStream?.read(10); // Should span chunk boundary
574
- expect(endOfFirst?.length).toBe(10);
575
- expect(endOfFirst?.subarray(0, 5).every((b) => b === 65)).toBe(true); // First 5 bytes: A's
576
- expect(endOfFirst?.subarray(5, 10).every((b) => b === 66)).toBe(true); // Next 5 bytes: B's
577
- // Seek to middle of second chunk
578
- await readStream?.seek(chunkSize + 1000);
579
- const middleOfSecond = await readStream?.read(100);
580
- expect(middleOfSecond?.length).toBe(100);
581
- expect(middleOfSecond?.every((b) => b === 66)).toBe(true); // All B's
582
- // Seek to third chunk
583
- await readStream?.seek(chunkSize * 2 + 100);
584
- const inThirdChunk = await readStream?.read(200);
585
- expect(inThirdChunk?.length).toBe(200);
586
- expect(inThirdChunk?.every((b) => b === 67)).toBe(true); // All C's
587
- });
588
- it("should handle seek error conditions", async () => {
589
- const fileId = (0, utils_1.newid)();
590
- const data = new Uint8Array(Buffer.from("Short test data", "utf8"));
591
- const metadata = {
592
- fileId,
593
- name: "error-test.txt",
594
- fileSize: data.length,
595
- mimeType: "text/plain",
596
- };
597
- await fileTable.saveFile(metadata, data);
598
- const readStream = await fileTable.openReadStream(fileId);
599
- expect(readStream).toBeTruthy();
600
- // Test negative seek position
601
- await expect(readStream?.seek(-1)).rejects.toThrow("Seek position cannot be negative");
602
- // Test seeking beyond file size
603
- await expect(readStream?.seek(data.length + 1)).rejects.toThrow("Seek position beyond file size");
604
- // Test seeking to exact end of file
605
- await readStream?.seek(data.length);
606
- expect(readStream?.getPosition()).toBe(data.length);
607
- expect(readStream?.getBytesRemaining()).toBe(0);
608
- // Should return null when reading at EOF
609
- const atEof = await readStream?.read();
610
- expect(atEof).toBeNull();
611
- });
612
- it("should handle large file streaming reads", async () => {
613
- // Create a large file with known pattern
614
- const fileId = (0, utils_1.newid)();
615
- const chunkSize = file_types_1.FILE_CHUNK_SIZE;
616
- const buffer = Buffer.alloc(chunkSize + 1000);
617
- // Fill with pattern: first chunk with 'A', remainder with 'B'
618
- buffer.fill(65, 0, chunkSize); // 'A'
619
- buffer.fill(66, chunkSize); // 'B'
620
- const largeData = new Uint8Array(buffer);
621
- const metadata = {
622
- fileId,
623
- name: "large-read.bin",
624
- fileSize: largeData.length,
625
- mimeType: "application/octet-stream",
626
- };
627
- await fileTable.saveFile(metadata, largeData);
628
- const readStream = await fileTable.openReadStream(fileId);
629
- expect(readStream).toBeTruthy();
630
- // Read first chunk
631
- const firstChunk = await readStream?.read(chunkSize);
632
- expect(firstChunk?.length).toBe(chunkSize);
633
- expect(firstChunk?.every((b) => b === 65)).toBe(true); // All 'A's
634
- // Read remainder
635
- const remainder = await readStream?.read();
636
- expect(remainder?.length).toBe(1000);
637
- expect(remainder?.every((b) => b === 66)).toBe(true); // All 'B's
638
- // Should be at EOF
639
- const eof = await readStream?.read();
640
- expect(eof).toBeNull();
641
- expect(readStream?.isEOF()).toBe(true);
642
- });
643
- it("should return null for non-existent file", async () => {
644
- const readStream = await fileTable.openReadStream("non-existent-file");
645
- expect(readStream).toBeNull();
646
- });
647
- it("should handle partial reads correctly", async () => {
648
- const fileId = (0, utils_1.newid)();
649
- const data = new Uint8Array(Buffer.from("ABCDEFGHIJKLMNOPQRSTUVWXYZ", "utf8"));
650
- const metadata = {
651
- fileId,
652
- name: "partial-read.txt",
653
- fileSize: data.length,
654
- mimeType: "text/plain",
655
- };
656
- await fileTable.saveFile(metadata, data);
657
- const readStream = await fileTable.openReadStream(fileId);
658
- expect(readStream).toBeTruthy();
659
- // Read in small chunks
660
- const chunk1 = await readStream?.read(5);
661
- expect(Buffer.from(chunk1).toString("utf8")).toBe("ABCDE");
662
- expect(readStream?.getPosition()).toBe(5);
663
- expect(readStream?.getBytesRemaining()).toBe(21);
664
- const chunk2 = await readStream?.read(10);
665
- expect(Buffer.from(chunk2).toString("utf8")).toBe("FGHIJKLMNO");
666
- expect(readStream?.getPosition()).toBe(15);
667
- const chunk3 = await readStream?.read(20); // More than remaining
668
- expect(Buffer.from(chunk3).toString("utf8")).toBe("PQRSTUVWXYZ");
669
- expect(readStream?.getPosition()).toBe(26);
670
- expect(readStream?.getBytesRemaining()).toBe(0);
671
- // Should be EOF now
672
- const eof = await readStream?.read();
673
- expect(eof).toBeNull();
674
- });
675
- it("should return null when chunk is unavailable", async () => {
676
- const fileId = (0, utils_1.newid)();
677
- const chunkHashes = ["missing_chunk_hash"];
678
- const fileHash = (0, keys_1.hashBytes)(new TextEncoder().encode(JSON.stringify(chunkHashes)));
679
- const metadata = {
680
- fileId,
681
- name: "missing-chunk.txt",
682
- fileSize: 50,
683
- fileHash,
684
- chunkHashes,
685
- };
686
- // Insert file metadata without storing the actual chunk
687
- await fileTable.dataSource.insert(metadata);
688
- // This should return null when the chunk can't be found
689
- const result = await fileTable.getFileContents(fileId);
690
- expect(result).toBeNull();
691
- });
692
- it("should throw when fileHash does not match chunk hashes", async () => {
693
- const fileId = (0, utils_1.newid)();
694
- const data = new Uint8Array(Buffer.from("Valid content", "utf8"));
695
- const metadata = {
696
- fileId,
697
- name: "tampered.txt",
698
- fileSize: data.length,
699
- mimeType: "text/plain",
700
- };
701
- // Save file normally to get valid chunks on disk
702
- const saved = await fileTable.saveFile(metadata, data);
703
- // Tamper with the fileHash in the database record
704
- const tamperedRecord = {
705
- ...saved,
706
- fileHash: "tampered-hash-value",
707
- };
708
- await fileTable.dataSource.save(tamperedRecord);
709
- // Reading should throw a file integrity error
710
- await expect(fileTable.getFileContents(fileId)).rejects.toThrow("File integrity check failed");
711
- });
712
- it("should skip fileHash verification when skipVerification is true", async () => {
713
- const fileId = (0, utils_1.newid)();
714
- const data = new Uint8Array(Buffer.from("Valid content", "utf8"));
715
- const metadata = {
716
- fileId,
717
- name: "skip-verify.txt",
718
- fileSize: data.length,
719
- mimeType: "text/plain",
720
- };
721
- // Save file normally to get valid chunks on disk
722
- const saved = await fileTable.saveFile(metadata, data);
723
- // Tamper with the fileHash in the database record
724
- const tamperedRecord = {
725
- ...saved,
726
- fileHash: "tampered-hash-value",
727
- };
728
- await fileTable.dataSource.save(tamperedRecord);
729
- // Reading with skipVerification should succeed
730
- const result = await fileTable.getFileContents(fileId, { skipVerification: true });
731
- expect(result).not.toBeNull();
732
- expect(new Uint8Array(result)).toEqual(data);
733
- });
734
- });
735
- describe("Round-trip streaming", () => {
736
- it("should write and read back identical data using streams", async () => {
737
- const fileId = (0, utils_1.newid)();
738
- const testData = "This is a round-trip test with streaming! ".repeat(1000);
739
- const originalBuffer = new Uint8Array(Buffer.from(testData, "utf8"));
740
- const metadata = {
741
- fileId,
742
- name: "roundtrip.txt",
743
- fileSize: 0,
744
- mimeType: "text/plain",
745
- };
746
- // Write using stream
747
- const writeStream = await fileTable.createWriteStream(metadata);
748
- // Write in chunks
749
- let offset = 0;
750
- const writeChunkSize = 1000;
751
- while (offset < originalBuffer.length) {
752
- const end = Math.min(offset + writeChunkSize, originalBuffer.length);
753
- const chunk = originalBuffer.subarray(offset, end);
754
- await writeStream.write(chunk);
755
- offset = end;
756
- }
757
- const savedFile = await writeStream.finalize();
758
- expect(savedFile.fileSize).toBe(originalBuffer.length);
759
- // Read back using stream
760
- const readStream = await fileTable.openReadStream(fileId);
761
- expect(readStream).toBeTruthy();
762
- const readBuffer = await readStream?.readAll();
763
- // Verify identical
764
- expect(readBuffer).not.toBeNull();
765
- expect(new Uint8Array(readBuffer)).toEqual(originalBuffer);
766
- expect(Buffer.from(readBuffer).toString("utf8")).toBe(testData);
767
- });
768
- });
769
- });
770
- });