@party-stack/foundry-ontology 0.18.0 → 0.19.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.
Files changed (254) hide show
  1. package/README.md +19 -0
  2. package/lib/adapter/convertLoadSubsetOptions.d.ts +2 -0
  3. package/lib/adapter/convertLoadSubsetOptions.d.ts.map +1 -1
  4. package/lib/adapter/convertLoadSubsetOptions.js +158 -191
  5. package/lib/adapter/convertLoadSubsetOptions.js.map +1 -1
  6. package/lib/adapter/createFoundryOntologyBackendAdapter.d.ts +17 -15
  7. package/lib/adapter/createFoundryOntologyBackendAdapter.d.ts.map +1 -1
  8. package/lib/adapter/createFoundryOntologyBackendAdapter.js +288 -400
  9. package/lib/adapter/createFoundryOntologyBackendAdapter.js.map +1 -1
  10. package/lib/adapter/foundryActionValidation.d.ts +17 -15
  11. package/lib/adapter/foundryActionValidation.d.ts.map +1 -1
  12. package/lib/adapter/foundryActionValidation.js +206 -305
  13. package/lib/adapter/foundryActionValidation.js.map +1 -1
  14. package/lib/adapter/foundryCodec.d.ts +10 -8
  15. package/lib/adapter/foundryCodec.d.ts.map +1 -1
  16. package/lib/adapter/foundryCodec.js +180 -287
  17. package/lib/adapter/foundryCodec.js.map +1 -1
  18. package/lib/adapter/foundryMediaId.d.ts +6 -4
  19. package/lib/adapter/foundryMediaId.d.ts.map +1 -1
  20. package/lib/adapter/foundryMediaId.js +31 -32
  21. package/lib/adapter/foundryMediaId.js.map +1 -1
  22. package/lib/adapter/index.d.ts +4 -4
  23. package/lib/adapter/index.js +4 -4
  24. package/lib/adapter/objectCollectionOptions.d.ts +18 -17
  25. package/lib/adapter/objectCollectionOptions.d.ts.map +1 -1
  26. package/lib/adapter/objectCollectionOptions.js +451 -538
  27. package/lib/adapter/objectCollectionOptions.js.map +1 -1
  28. package/lib/config/applyAttachmentConstraintOverrides.d.ts +6 -4
  29. package/lib/config/applyAttachmentConstraintOverrides.d.ts.map +1 -1
  30. package/lib/config/applyAttachmentConstraintOverrides.js +45 -49
  31. package/lib/config/applyAttachmentConstraintOverrides.js.map +1 -1
  32. package/lib/config/index.d.ts +17 -15
  33. package/lib/config/index.d.ts.map +1 -1
  34. package/lib/config/index.js +56 -81
  35. package/lib/config/index.js.map +1 -1
  36. package/lib/connection.d.ts +28 -26
  37. package/lib/connection.d.ts.map +1 -1
  38. package/lib/connection.js +183 -215
  39. package/lib/connection.js.map +1 -1
  40. package/lib/index.d.ts +9 -5
  41. package/lib/index.js +9 -5
  42. package/lib/installation/createFoundryBackendInstallation.d.ts +21 -20
  43. package/lib/installation/createFoundryBackendInstallation.d.ts.map +1 -1
  44. package/lib/installation/createFoundryBackendInstallation.js +52 -62
  45. package/lib/installation/createFoundryBackendInstallation.js.map +1 -1
  46. package/lib/installation/index.d.ts +2 -2
  47. package/lib/installation/index.js +1 -2
  48. package/lib/meta/actionTypeCollectionOptions.d.ts +8 -6
  49. package/lib/meta/actionTypeCollectionOptions.d.ts.map +1 -1
  50. package/lib/meta/actionTypeCollectionOptions.js +49 -49
  51. package/lib/meta/actionTypeCollectionOptions.js.map +1 -1
  52. package/lib/meta/convertActionTypeLoadSubsetOptions.d.ts +3 -1
  53. package/lib/meta/convertActionTypeLoadSubsetOptions.d.ts.map +1 -1
  54. package/lib/meta/convertActionTypeLoadSubsetOptions.js +69 -75
  55. package/lib/meta/convertActionTypeLoadSubsetOptions.js.map +1 -1
  56. package/lib/meta/convertMetaActionType.d.ts +7 -5
  57. package/lib/meta/convertMetaActionType.d.ts.map +1 -1
  58. package/lib/meta/convertMetaActionType.js +458 -581
  59. package/lib/meta/convertMetaActionType.js.map +1 -1
  60. package/lib/meta/convertMetaLinkType.d.ts +4 -2
  61. package/lib/meta/convertMetaLinkType.d.ts.map +1 -1
  62. package/lib/meta/convertMetaLinkType.js +37 -43
  63. package/lib/meta/convertMetaLinkType.js.map +1 -1
  64. package/lib/meta/convertMetaObjectType.d.ts +4 -2
  65. package/lib/meta/convertMetaObjectType.d.ts.map +1 -1
  66. package/lib/meta/convertMetaObjectType.js +34 -35
  67. package/lib/meta/convertMetaObjectType.js.map +1 -1
  68. package/lib/meta/convertMetaQueryFunctionType.d.ts +4 -2
  69. package/lib/meta/convertMetaQueryFunctionType.d.ts.map +1 -1
  70. package/lib/meta/convertMetaQueryFunctionType.js +113 -94
  71. package/lib/meta/convertMetaQueryFunctionType.js.map +1 -1
  72. package/lib/meta/convertMetaTypeDef.d.ts +4 -2
  73. package/lib/meta/convertMetaTypeDef.d.ts.map +1 -1
  74. package/lib/meta/convertMetaTypeDef.js +178 -176
  75. package/lib/meta/convertMetaTypeDef.js.map +1 -1
  76. package/lib/meta/convertMetaValueType.d.ts +4 -2
  77. package/lib/meta/convertMetaValueType.d.ts.map +1 -1
  78. package/lib/meta/convertMetaValueType.js +11 -7
  79. package/lib/meta/convertMetaValueType.js.map +1 -1
  80. package/lib/meta/convertOmsActionParameterMetadata.d.ts +4 -2
  81. package/lib/meta/convertOmsActionParameterMetadata.d.ts.map +1 -1
  82. package/lib/meta/convertOmsActionParameterMetadata.js +107 -163
  83. package/lib/meta/convertOmsActionParameterMetadata.js.map +1 -1
  84. package/lib/meta/createFoundryMetaOntologyBackendAdapter.d.ts +7 -5
  85. package/lib/meta/createFoundryMetaOntologyBackendAdapter.d.ts.map +1 -1
  86. package/lib/meta/createFoundryMetaOntologyBackendAdapter.js +39 -41
  87. package/lib/meta/createFoundryMetaOntologyBackendAdapter.js.map +1 -1
  88. package/lib/meta/entityCollectionOptions.d.ts +12 -10
  89. package/lib/meta/entityCollectionOptions.d.ts.map +1 -1
  90. package/lib/meta/entityCollectionOptions.js +59 -62
  91. package/lib/meta/entityCollectionOptions.js.map +1 -1
  92. package/lib/meta/foundryAttachmentMetadata.d.ts +2 -0
  93. package/lib/meta/foundryAttachmentMetadata.d.ts.map +1 -1
  94. package/lib/meta/foundryAttachmentMetadata.js +13 -14
  95. package/lib/meta/foundryAttachmentMetadata.js.map +1 -1
  96. package/lib/meta/index.d.ts +5 -5
  97. package/lib/meta/index.js +5 -5
  98. package/lib/meta/loadActionTypeOmsMetadata.d.ts +5 -3
  99. package/lib/meta/loadActionTypeOmsMetadata.d.ts.map +1 -1
  100. package/lib/meta/loadActionTypeOmsMetadata.js +102 -139
  101. package/lib/meta/loadActionTypeOmsMetadata.js.map +1 -1
  102. package/lib/meta/queryFunctionTypeCollectionOptions.d.ts +7 -5
  103. package/lib/meta/queryFunctionTypeCollectionOptions.d.ts.map +1 -1
  104. package/lib/meta/queryFunctionTypeCollectionOptions.js +47 -49
  105. package/lib/meta/queryFunctionTypeCollectionOptions.js.map +1 -1
  106. package/lib/stagedWrites.d.ts +26 -24
  107. package/lib/stagedWrites.d.ts.map +1 -1
  108. package/lib/stagedWrites.js +55 -66
  109. package/lib/stagedWrites.js.map +1 -1
  110. package/lib/users/createFoundryAdminOntologyRoute.d.ts +47 -17366
  111. package/lib/users/createFoundryAdminOntologyRoute.d.ts.map +1 -1
  112. package/lib/users/createFoundryAdminOntologyRoute.js +29 -37
  113. package/lib/users/createFoundryAdminOntologyRoute.js.map +1 -1
  114. package/lib/users/createFoundryUsersIntegration.d.ts +6 -4
  115. package/lib/users/createFoundryUsersIntegration.d.ts.map +1 -1
  116. package/lib/users/createFoundryUsersIntegration.js +28 -30
  117. package/lib/users/createFoundryUsersIntegration.js.map +1 -1
  118. package/lib/users/foundryUser.d.ts +4 -2
  119. package/lib/users/foundryUser.d.ts.map +1 -1
  120. package/lib/users/foundryUser.js +69 -74
  121. package/lib/users/foundryUser.js.map +1 -1
  122. package/lib/users/index.d.ts +5 -5
  123. package/lib/users/index.js +5 -5
  124. package/lib/users/userCollectionOptions.d.ts +13 -11
  125. package/lib/users/userCollectionOptions.d.ts.map +1 -1
  126. package/lib/users/userCollectionOptions.js +82 -100
  127. package/lib/users/userCollectionOptions.js.map +1 -1
  128. package/lib/utils/AsyncIterable.d.ts +2 -0
  129. package/lib/utils/AsyncIterable.d.ts.map +1 -1
  130. package/lib/utils/AsyncIterable.js +26 -34
  131. package/lib/utils/AsyncIterable.js.map +1 -1
  132. package/lib/utils/actionTypeName.d.ts +2 -0
  133. package/lib/utils/actionTypeName.d.ts.map +1 -1
  134. package/lib/utils/actionTypeName.js +9 -7
  135. package/lib/utils/actionTypeName.js.map +1 -1
  136. package/lib/utils/chunk.d.ts +2 -0
  137. package/lib/utils/chunk.d.ts.map +1 -1
  138. package/lib/utils/chunk.js +8 -6
  139. package/lib/utils/chunk.js.map +1 -1
  140. package/package.json +21 -27
  141. package/lib/adapter/convertLoadSubsetOptions.test.d.ts +0 -2
  142. package/lib/adapter/convertLoadSubsetOptions.test.d.ts.map +0 -1
  143. package/lib/adapter/convertLoadSubsetOptions.test.js +0 -213
  144. package/lib/adapter/convertLoadSubsetOptions.test.js.map +0 -1
  145. package/lib/adapter/createFoundryOntologyBackendAdapter.test.d.ts +0 -2
  146. package/lib/adapter/createFoundryOntologyBackendAdapter.test.d.ts.map +0 -1
  147. package/lib/adapter/createFoundryOntologyBackendAdapter.test.js +0 -628
  148. package/lib/adapter/createFoundryOntologyBackendAdapter.test.js.map +0 -1
  149. package/lib/adapter/foundryActionValidation.test.d.ts +0 -2
  150. package/lib/adapter/foundryActionValidation.test.d.ts.map +0 -1
  151. package/lib/adapter/foundryActionValidation.test.js +0 -243
  152. package/lib/adapter/foundryActionValidation.test.js.map +0 -1
  153. package/lib/adapter/foundryCodec.test.d.ts +0 -2
  154. package/lib/adapter/foundryCodec.test.d.ts.map +0 -1
  155. package/lib/adapter/foundryCodec.test.js +0 -137
  156. package/lib/adapter/foundryCodec.test.js.map +0 -1
  157. package/lib/adapter/foundryMediaId.test.d.ts +0 -2
  158. package/lib/adapter/foundryMediaId.test.d.ts.map +0 -1
  159. package/lib/adapter/foundryMediaId.test.js +0 -23
  160. package/lib/adapter/foundryMediaId.test.js.map +0 -1
  161. package/lib/adapter/index.d.ts.map +0 -1
  162. package/lib/adapter/index.js.map +0 -1
  163. package/lib/adapter/objectCollectionOptions.test.d.ts +0 -2
  164. package/lib/adapter/objectCollectionOptions.test.d.ts.map +0 -1
  165. package/lib/adapter/objectCollectionOptions.test.js +0 -1290
  166. package/lib/adapter/objectCollectionOptions.test.js.map +0 -1
  167. package/lib/config/connectionPull.test.d.ts +0 -2
  168. package/lib/config/connectionPull.test.d.ts.map +0 -1
  169. package/lib/config/connectionPull.test.js +0 -151
  170. package/lib/config/connectionPull.test.js.map +0 -1
  171. package/lib/config/index.test.d.ts +0 -2
  172. package/lib/config/index.test.d.ts.map +0 -1
  173. package/lib/config/index.test.js +0 -182
  174. package/lib/config/index.test.js.map +0 -1
  175. package/lib/connection.test.d.ts +0 -2
  176. package/lib/connection.test.d.ts.map +0 -1
  177. package/lib/connection.test.js +0 -162
  178. package/lib/connection.test.js.map +0 -1
  179. package/lib/index.d.ts.map +0 -1
  180. package/lib/index.js.map +0 -1
  181. package/lib/installation/createFoundryBackendInstallation.test.d.ts +0 -2
  182. package/lib/installation/createFoundryBackendInstallation.test.d.ts.map +0 -1
  183. package/lib/installation/createFoundryBackendInstallation.test.js +0 -83
  184. package/lib/installation/createFoundryBackendInstallation.test.js.map +0 -1
  185. package/lib/installation/index.d.ts.map +0 -1
  186. package/lib/installation/index.js.map +0 -1
  187. package/lib/meta/actionTypeCollectionOptions.test.d.ts +0 -2
  188. package/lib/meta/actionTypeCollectionOptions.test.d.ts.map +0 -1
  189. package/lib/meta/actionTypeCollectionOptions.test.js +0 -24
  190. package/lib/meta/actionTypeCollectionOptions.test.js.map +0 -1
  191. package/lib/meta/actionTypeIdQuery.test.d.ts +0 -2
  192. package/lib/meta/actionTypeIdQuery.test.d.ts.map +0 -1
  193. package/lib/meta/actionTypeIdQuery.test.js +0 -131
  194. package/lib/meta/actionTypeIdQuery.test.js.map +0 -1
  195. package/lib/meta/convertActionTypeLoadSubsetOptions.test.d.ts +0 -2
  196. package/lib/meta/convertActionTypeLoadSubsetOptions.test.d.ts.map +0 -1
  197. package/lib/meta/convertActionTypeLoadSubsetOptions.test.js +0 -138
  198. package/lib/meta/convertActionTypeLoadSubsetOptions.test.js.map +0 -1
  199. package/lib/meta/convertMetaActionType.test.d.ts +0 -2
  200. package/lib/meta/convertMetaActionType.test.d.ts.map +0 -1
  201. package/lib/meta/convertMetaActionType.test.js +0 -1565
  202. package/lib/meta/convertMetaActionType.test.js.map +0 -1
  203. package/lib/meta/convertMetaLinkType.test.d.ts +0 -2
  204. package/lib/meta/convertMetaLinkType.test.d.ts.map +0 -1
  205. package/lib/meta/convertMetaLinkType.test.js +0 -110
  206. package/lib/meta/convertMetaLinkType.test.js.map +0 -1
  207. package/lib/meta/convertMetaObjectType.test.d.ts +0 -2
  208. package/lib/meta/convertMetaObjectType.test.d.ts.map +0 -1
  209. package/lib/meta/convertMetaObjectType.test.js +0 -250
  210. package/lib/meta/convertMetaObjectType.test.js.map +0 -1
  211. package/lib/meta/convertMetaQueryFunctionType.test.d.ts +0 -2
  212. package/lib/meta/convertMetaQueryFunctionType.test.d.ts.map +0 -1
  213. package/lib/meta/convertMetaQueryFunctionType.test.js +0 -134
  214. package/lib/meta/convertMetaQueryFunctionType.test.js.map +0 -1
  215. package/lib/meta/createFoundryMetaOntologyBackendAdapter.test.d.ts +0 -2
  216. package/lib/meta/createFoundryMetaOntologyBackendAdapter.test.d.ts.map +0 -1
  217. package/lib/meta/createFoundryMetaOntologyBackendAdapter.test.js +0 -49
  218. package/lib/meta/createFoundryMetaOntologyBackendAdapter.test.js.map +0 -1
  219. package/lib/meta/entityCollectionOptions.test.d.ts +0 -2
  220. package/lib/meta/entityCollectionOptions.test.d.ts.map +0 -1
  221. package/lib/meta/entityCollectionOptions.test.js +0 -98
  222. package/lib/meta/entityCollectionOptions.test.js.map +0 -1
  223. package/lib/meta/index.d.ts.map +0 -1
  224. package/lib/meta/index.js.map +0 -1
  225. package/lib/meta/loadActionTypeOmsMetadata.test.d.ts +0 -2
  226. package/lib/meta/loadActionTypeOmsMetadata.test.d.ts.map +0 -1
  227. package/lib/meta/loadActionTypeOmsMetadata.test.js +0 -218
  228. package/lib/meta/loadActionTypeOmsMetadata.test.js.map +0 -1
  229. package/lib/stagedWrites.test.d.ts +0 -2
  230. package/lib/stagedWrites.test.d.ts.map +0 -1
  231. package/lib/stagedWrites.test.js +0 -71
  232. package/lib/stagedWrites.test.js.map +0 -1
  233. package/lib/users/createFoundryUsersIntegration.test.d.ts +0 -2
  234. package/lib/users/createFoundryUsersIntegration.test.d.ts.map +0 -1
  235. package/lib/users/createFoundryUsersIntegration.test.js +0 -32
  236. package/lib/users/createFoundryUsersIntegration.test.js.map +0 -1
  237. package/lib/users/foundryUser.test.d.ts +0 -2
  238. package/lib/users/foundryUser.test.d.ts.map +0 -1
  239. package/lib/users/foundryUser.test.js +0 -45
  240. package/lib/users/foundryUser.test.js.map +0 -1
  241. package/lib/users/index.d.ts.map +0 -1
  242. package/lib/users/index.js.map +0 -1
  243. package/lib/users/userCollectionOptions.test.d.ts +0 -2
  244. package/lib/users/userCollectionOptions.test.d.ts.map +0 -1
  245. package/lib/users/userCollectionOptions.test.js +0 -18
  246. package/lib/users/userCollectionOptions.test.js.map +0 -1
  247. package/lib/utils/AsyncIterable.test.d.ts +0 -2
  248. package/lib/utils/AsyncIterable.test.d.ts.map +0 -1
  249. package/lib/utils/AsyncIterable.test.js +0 -17
  250. package/lib/utils/AsyncIterable.test.js.map +0 -1
  251. package/lib/utils/actionTypeName.test.d.ts +0 -2
  252. package/lib/utils/actionTypeName.test.d.ts.map +0 -1
  253. package/lib/utils/actionTypeName.test.js +0 -30
  254. package/lib/utils/actionTypeName.test.js.map +0 -1
@@ -1,1290 +0,0 @@
1
- import { o } from "@party-stack/ontology";
2
- import { eq, gt, IR } from "@tanstack/db";
3
- import { beforeEach, describe, expect, it, vi } from "vitest";
4
- const mockState = vi.hoisted(() => ({
5
- getEditsHistory: vi.fn(),
6
- getObject: vi.fn(),
7
- search: vi.fn(),
8
- getObjectSetWatcherManager: vi.fn(),
9
- unsubscribe: vi.fn(),
10
- subscribeCallback: undefined,
11
- }));
12
- vi.mock("@osdk/foundry.ontologies", () => ({
13
- OntologyObjectsV2: {
14
- get: mockState.getObject,
15
- search: mockState.search,
16
- },
17
- ObjectTypesV2: {
18
- getEditsHistory: mockState.getEditsHistory,
19
- },
20
- }));
21
- vi.mock("@party-stack/foundry-object-set-watcher", () => ({
22
- getObjectSetWatcherManager: mockState.getObjectSetWatcherManager,
23
- }));
24
- import { createFoundryCodec } from "./foundryCodec.js";
25
- import { objectCollectionOptions } from "./objectCollectionOptions.js";
26
- function createSyncHarness(initialObjects = [], opts = {}) {
27
- const syncedData = new Map(initialObjects.map((object) => [object.employeeId, object]));
28
- const writes = [];
29
- const collectionMetadata = opts.collectionMetadata ?? new Map();
30
- const transactions = [];
31
- let pendingTransaction;
32
- const begin = vi.fn(() => {
33
- if (pendingTransaction)
34
- throw new Error("A sync transaction is already active.");
35
- pendingTransaction = {
36
- writes: [],
37
- collectionMetadata: new Map(),
38
- };
39
- });
40
- const commit = vi.fn(() => {
41
- if (!pendingTransaction)
42
- throw new Error("No sync transaction is active.");
43
- for (const [key, value] of pendingTransaction.collectionMetadata) {
44
- collectionMetadata.set(key, value);
45
- }
46
- transactions.push(pendingTransaction);
47
- pendingTransaction = undefined;
48
- return opts.commitReceipt?.() ?? true;
49
- });
50
- const truncate = vi.fn(() => {
51
- if (!pendingTransaction) {
52
- throw new Error("No sync transaction is active.");
53
- }
54
- syncedData.clear();
55
- });
56
- const { sync: syncConfig, utils } = objectCollectionOptions({
57
- client: {
58
- baseUrl: "https://example.com",
59
- fetch: opts.fetch ?? vi.fn(),
60
- ontologyRid: "ri.ontology.main",
61
- tokenProvider: () => Promise.resolve("token"),
62
- },
63
- objectType: "Employee",
64
- primaryKeyProperty: "employeeId",
65
- selectedProperties: [
66
- "employeeId",
67
- "name",
68
- "status",
69
- "priority",
70
- ],
71
- live: opts.live,
72
- decodeObject: opts.decodeObject,
73
- decodeEditObject: opts.decodeEditObject,
74
- });
75
- const handle = syncConfig.sync({
76
- begin,
77
- collection: {
78
- get: (key) => syncedData.get(key),
79
- has: (key) => syncedData.has(key),
80
- *keys() {
81
- yield* syncedData.keys();
82
- },
83
- syncedData,
84
- },
85
- commit,
86
- markError: vi.fn(),
87
- markReady: vi.fn(),
88
- metadata: {
89
- persistence: null,
90
- collection: {
91
- get: (key) => pendingTransaction?.collectionMetadata.get(key) ?? collectionMetadata.get(key),
92
- set: (key, value) => {
93
- if (!pendingTransaction)
94
- throw new Error("No sync transaction is active.");
95
- pendingTransaction.collectionMetadata.set(key, value);
96
- },
97
- delete: vi.fn(),
98
- list: () => [...collectionMetadata].map(([key, value]) => ({ key, value })),
99
- },
100
- row: {
101
- get: vi.fn(),
102
- set: vi.fn(),
103
- delete: vi.fn(),
104
- },
105
- },
106
- truncate,
107
- write: (mutation) => {
108
- if (!pendingTransaction)
109
- throw new Error("No sync transaction is active.");
110
- const key = mutation.type === "delete"
111
- ? mutation.key
112
- : mutation.value
113
- .employeeId;
114
- const observedWrite = { type: mutation.type, primaryKey: key };
115
- writes.push(observedWrite);
116
- pendingTransaction.writes.push(observedWrite);
117
- if (mutation.type === "delete") {
118
- syncedData.delete(key);
119
- }
120
- else {
121
- const value = mutation.value;
122
- syncedData.set(key, value);
123
- }
124
- },
125
- });
126
- return {
127
- begin,
128
- cleanup: handle.cleanup,
129
- collectionMetadata,
130
- commit,
131
- loadSubset: handle.loadSubset,
132
- syncedData,
133
- transactions,
134
- truncate,
135
- utils,
136
- writes,
137
- };
138
- }
139
- describe("objectCollectionOptions", () => {
140
- beforeEach(() => {
141
- mockState.getEditsHistory.mockReset();
142
- mockState.getObject.mockReset();
143
- mockState.search.mockReset();
144
- mockState.getObjectSetWatcherManager.mockReset();
145
- mockState.unsubscribe.mockReset();
146
- mockState.subscribeCallback = undefined;
147
- mockState.getObjectSetWatcherManager.mockReturnValue({
148
- subscribe: (_objectSet, callback) => {
149
- mockState.subscribeCallback = callback;
150
- return () => {
151
- mockState.subscribeCallback = undefined;
152
- mockState.unsubscribe();
153
- };
154
- },
155
- });
156
- });
157
- it("constructs and cleans up the object-set watcher by default", () => {
158
- const harness = createSyncHarness();
159
- expect(mockState.getObjectSetWatcherManager).toHaveBeenCalledTimes(1);
160
- harness.cleanup();
161
- expect(mockState.unsubscribe).toHaveBeenCalledTimes(1);
162
- });
163
- it("loads subsets without constructing an object-set watcher when live is false", async () => {
164
- mockState.search.mockResolvedValue({
165
- data: [{ employeeId: 1, name: "Ada" }],
166
- nextPageToken: undefined,
167
- });
168
- const harness = createSyncHarness([], { live: false });
169
- expect(mockState.getObjectSetWatcherManager).not.toHaveBeenCalled();
170
- await harness.loadSubset({});
171
- expect(harness.syncedData.get(1)).toEqual({
172
- employeeId: 1,
173
- name: "Ada",
174
- });
175
- harness.cleanup();
176
- expect(mockState.unsubscribe).not.toHaveBeenCalled();
177
- });
178
- it("supports bounded edit-history observation without a watcher when live is false", async () => {
179
- mockState.getEditsHistory.mockResolvedValue({
180
- data: [
181
- {
182
- objectPrimaryKey: { employeeId: 2 },
183
- operationId: "op-non-live",
184
- actionTypeRid: "action-1",
185
- userId: "user-1",
186
- timestamp: "2099-03-12T12:00:00.000Z",
187
- edit: {
188
- type: "createEdit",
189
- properties: {
190
- employeeId: 2,
191
- name: "Grace",
192
- },
193
- },
194
- },
195
- ],
196
- nextPageToken: undefined,
197
- });
198
- const harness = createSyncHarness([], { live: false });
199
- await expect(harness.utils.awaitOperationId("op-non-live")).resolves.toBeUndefined();
200
- expect(mockState.getObjectSetWatcherManager).not.toHaveBeenCalled();
201
- expect(harness.syncedData.get(2)).toEqual({
202
- employeeId: 2,
203
- name: "Grace",
204
- });
205
- harness.cleanup();
206
- });
207
- it("keeps identical abortable subset loads independent", async () => {
208
- const requests = [];
209
- const fetchImpl = vi.fn((_input, init) => {
210
- return new Promise((resolve, reject) => {
211
- const request = { resolve, reject };
212
- requests.push(request);
213
- const signal = init?.signal;
214
- signal?.addEventListener("abort", () => reject(signal.reason instanceof Error
215
- ? signal.reason
216
- : new DOMException("Request aborted.", "AbortError")), { once: true });
217
- });
218
- });
219
- mockState.search.mockImplementation((client) => client.fetch("https://example.com/search"));
220
- const harness = createSyncHarness([], { fetch: fetchImpl });
221
- const controllerA = new AbortController();
222
- const controllerB = new AbortController();
223
- const requestA = harness.loadSubset({
224
- limit: 1,
225
- signal: controllerA.signal,
226
- });
227
- const requestB = harness.loadSubset({
228
- limit: 1,
229
- signal: controllerB.signal,
230
- });
231
- expect(requestA).not.toBe(requestB);
232
- expect(mockState.search).toHaveBeenCalledTimes(2);
233
- controllerA.abort(new DOMException("Request A canceled.", "AbortError"));
234
- requests[1].resolve({
235
- data: [{ employeeId: "employee-b", name: "Request B" }],
236
- nextPageToken: undefined,
237
- });
238
- await expect(requestA).rejects.toMatchObject({ name: "AbortError" });
239
- await expect(requestB).resolves.toBeUndefined();
240
- expect(harness.syncedData.get("employee-b")).toMatchObject({
241
- name: "Request B",
242
- });
243
- harness.cleanup();
244
- });
245
- it("reconciles modify edits from history on watcher open", async () => {
246
- mockState.getEditsHistory.mockResolvedValue({
247
- data: [
248
- {
249
- objectPrimaryKey: { employeeId: 1 },
250
- operationId: "op-1",
251
- actionTypeRid: "action-1",
252
- userId: "user-1",
253
- timestamp: "2099-03-12T12:00:00.000Z",
254
- edit: {
255
- type: "modifyEdit",
256
- previousProperties: {},
257
- properties: {
258
- employeeId: 1,
259
- name: "Updated Employee",
260
- },
261
- },
262
- },
263
- ],
264
- nextPageToken: undefined,
265
- });
266
- const harness = createSyncHarness([
267
- {
268
- employeeId: 1,
269
- name: "Before",
270
- department: "Engineering",
271
- },
272
- ]);
273
- mockState.subscribeCallback?.({ type: "state", status: "open" });
274
- await vi.waitFor(() => {
275
- expect(mockState.getEditsHistory).toHaveBeenCalledTimes(1);
276
- expect(harness.syncedData.get(1)).toEqual({
277
- employeeId: 1,
278
- name: "Updated Employee",
279
- department: "Engineering",
280
- });
281
- });
282
- expect(mockState.getObject).not.toHaveBeenCalled();
283
- expect(harness.begin.mock.calls).toEqual([
284
- [],
285
- [],
286
- ]);
287
- harness.cleanup();
288
- });
289
- it("applies delete edits from history", async () => {
290
- mockState.getEditsHistory.mockResolvedValue({
291
- data: [
292
- {
293
- objectPrimaryKey: { employeeId: 2 },
294
- operationId: "op-2",
295
- actionTypeRid: "action-1",
296
- userId: "user-1",
297
- timestamp: "2099-03-12T12:01:00.000Z",
298
- edit: {
299
- type: "deleteEdit",
300
- previousProperties: {},
301
- },
302
- },
303
- ],
304
- nextPageToken: undefined,
305
- });
306
- const harness = createSyncHarness([{ employeeId: 2, name: "Should Stay" }]);
307
- mockState.subscribeCallback?.({ type: "state", status: "open" });
308
- await vi.waitFor(() => {
309
- expect(mockState.getEditsHistory).toHaveBeenCalledTimes(1);
310
- expect(harness.syncedData.has(2)).toBe(false);
311
- });
312
- expect(mockState.getObject).not.toHaveBeenCalled();
313
- harness.cleanup();
314
- });
315
- it("deletes objects hydrated after source sync starts", async () => {
316
- mockState.getEditsHistory.mockResolvedValue({
317
- data: [
318
- {
319
- objectPrimaryKey: {
320
- employeeId: 2,
321
- },
322
- operationId: "op-hydrated-delete",
323
- actionTypeRid: "action-1",
324
- userId: "user-1",
325
- timestamp: "2099-03-12T12:01:00.000Z",
326
- edit: {
327
- type: "deleteEdit",
328
- previousProperties: {},
329
- },
330
- },
331
- ],
332
- nextPageToken: undefined,
333
- });
334
- const harness = createSyncHarness();
335
- // Persisted on-demand hydration happens after the source sync closure
336
- // captures its initial state.
337
- harness.syncedData.set(2, {
338
- employeeId: 2,
339
- name: "Persisted stale object",
340
- });
341
- mockState.subscribeCallback?.({
342
- type: "state",
343
- status: "open",
344
- });
345
- await vi.waitFor(() => {
346
- expect(harness.syncedData.has(2)).toBe(false);
347
- });
348
- expect(harness.writes).toContainEqual({
349
- type: "delete",
350
- primaryKey: 2,
351
- });
352
- harness.cleanup();
353
- });
354
- it("treats watcher REMOVED events as a history catch-up signal", async () => {
355
- mockState.getEditsHistory.mockResolvedValue({
356
- data: [
357
- {
358
- objectPrimaryKey: { employeeId: 5 },
359
- operationId: "op-5",
360
- actionTypeRid: "action-1",
361
- userId: "user-1",
362
- timestamp: "2099-03-12T12:00:00.000Z",
363
- edit: {
364
- type: "createEdit",
365
- properties: {
366
- employeeId: 5,
367
- name: "Employee Five",
368
- },
369
- },
370
- },
371
- ],
372
- nextPageToken: undefined,
373
- });
374
- mockState.getEditsHistory.mockResolvedValueOnce({
375
- data: [
376
- {
377
- objectPrimaryKey: { employeeId: 5 },
378
- operationId: "op-5",
379
- actionTypeRid: "action-1",
380
- userId: "user-1",
381
- timestamp: "2099-03-12T12:00:00.000Z",
382
- edit: {
383
- type: "createEdit",
384
- properties: {
385
- employeeId: 5,
386
- name: "Employee Five",
387
- },
388
- },
389
- },
390
- ],
391
- nextPageToken: undefined,
392
- });
393
- mockState.getEditsHistory.mockResolvedValueOnce({
394
- data: [
395
- {
396
- objectPrimaryKey: { employeeId: 5 },
397
- operationId: "op-6",
398
- actionTypeRid: "action-1",
399
- userId: "user-1",
400
- timestamp: "2099-03-12T12:01:00.000Z",
401
- edit: {
402
- type: "deleteEdit",
403
- previousProperties: {},
404
- },
405
- },
406
- ],
407
- nextPageToken: undefined,
408
- });
409
- const harness = createSyncHarness();
410
- mockState.subscribeCallback?.({ type: "state", status: "open" });
411
- await vi.waitFor(() => {
412
- expect(harness.syncedData.has(5)).toBe(true);
413
- });
414
- mockState.subscribeCallback?.({
415
- type: "change",
416
- updates: [
417
- {
418
- type: "object",
419
- state: "REMOVED",
420
- object: { employeeId: 5 },
421
- },
422
- ],
423
- });
424
- await vi.waitFor(() => {
425
- expect(mockState.getEditsHistory).toHaveBeenCalledTimes(2);
426
- expect(harness.syncedData.has(5)).toBe(false);
427
- expect(harness.writes).toContainEqual({ type: "delete", primaryKey: 5 });
428
- });
429
- harness.cleanup();
430
- });
431
- it("dedupes inclusive timestamp results across catch-up runs", async () => {
432
- mockState.getEditsHistory
433
- .mockResolvedValueOnce({
434
- data: [
435
- {
436
- objectPrimaryKey: { employeeId: 1 },
437
- operationId: "op-1",
438
- actionTypeRid: "action-1",
439
- userId: "user-1",
440
- timestamp: "2099-03-12T12:00:00.000Z",
441
- edit: {
442
- type: "modifyEdit",
443
- previousProperties: {},
444
- properties: {
445
- employeeId: 1,
446
- name: "Employee One",
447
- },
448
- },
449
- },
450
- ],
451
- nextPageToken: undefined,
452
- })
453
- .mockResolvedValueOnce({
454
- data: [
455
- {
456
- objectPrimaryKey: { employeeId: 1 },
457
- operationId: "op-1",
458
- actionTypeRid: "action-1",
459
- userId: "user-1",
460
- timestamp: "2099-03-12T12:00:00.000Z",
461
- edit: {
462
- type: "modifyEdit",
463
- previousProperties: {},
464
- properties: {
465
- employeeId: 1,
466
- name: "Employee One",
467
- },
468
- },
469
- },
470
- {
471
- objectPrimaryKey: { employeeId: 2 },
472
- operationId: "op-2",
473
- actionTypeRid: "action-1",
474
- userId: "user-1",
475
- timestamp: "2099-03-12T12:00:00.000Z",
476
- edit: {
477
- type: "modifyEdit",
478
- previousProperties: {},
479
- properties: {
480
- employeeId: 2,
481
- name: "Employee Two",
482
- },
483
- },
484
- },
485
- ],
486
- nextPageToken: undefined,
487
- });
488
- const harness = createSyncHarness();
489
- mockState.subscribeCallback?.({ type: "state", status: "open" });
490
- mockState.subscribeCallback?.({
491
- type: "change",
492
- updates: [
493
- {
494
- type: "object",
495
- state: "ADDED_OR_UPDATED",
496
- object: { employeeId: 2, name: "Employee Two" },
497
- },
498
- ],
499
- });
500
- await vi.waitFor(() => {
501
- expect(mockState.getEditsHistory).toHaveBeenCalledTimes(2);
502
- expect(harness.syncedData.get(1)).toEqual({
503
- employeeId: 1,
504
- name: "Employee One",
505
- });
506
- expect(harness.syncedData.get(2)).toEqual({
507
- employeeId: 2,
508
- name: "Employee Two",
509
- });
510
- });
511
- expect(mockState.getObject).not.toHaveBeenCalled();
512
- harness.cleanup();
513
- });
514
- it("restores a durable same-timestamp cursor and catches downtime edits once", async () => {
515
- const timestamp = "2099-03-12T12:00:00.000Z";
516
- const firstEntry = {
517
- objectPrimaryKey: { employeeId: 1 },
518
- operationId: "op-1",
519
- actionTypeRid: "action-1",
520
- userId: "user-1",
521
- timestamp,
522
- edit: {
523
- type: "modifyEdit",
524
- previousProperties: {},
525
- properties: {
526
- employeeId: 1,
527
- name: "Employee One",
528
- },
529
- },
530
- };
531
- const downtimeEntry = {
532
- objectPrimaryKey: { employeeId: 2 },
533
- operationId: "op-2",
534
- actionTypeRid: "action-1",
535
- userId: "user-1",
536
- timestamp,
537
- edit: {
538
- type: "modifyEdit",
539
- previousProperties: {},
540
- properties: {
541
- employeeId: 2,
542
- name: "Employee Two",
543
- },
544
- },
545
- };
546
- const collectionMetadata = new Map();
547
- mockState.getEditsHistory.mockResolvedValue({
548
- data: [firstEntry],
549
- nextPageToken: undefined,
550
- });
551
- const first = createSyncHarness([], { collectionMetadata });
552
- mockState.subscribeCallback?.({ type: "state", status: "open" });
553
- await vi.waitFor(() => {
554
- expect(first.syncedData.get(1)).toMatchObject({ name: "Employee One" });
555
- });
556
- const persistedCursor = [...collectionMetadata.values()][0];
557
- expect(persistedCursor).toMatchObject({
558
- version: 1,
559
- timestamp,
560
- });
561
- expect(persistedCursor.seenEntryKeysAtTimestamp).toHaveLength(1);
562
- expect(first.transactions.at(-1)).toMatchObject({
563
- writes: [{ type: "insert", primaryKey: 1 }],
564
- });
565
- expect(first.transactions.at(-1)?.collectionMetadata.size).toBe(1);
566
- const reloadedObjects = [...first.syncedData.values()];
567
- first.cleanup();
568
- mockState.getEditsHistory.mockReset();
569
- mockState.getEditsHistory.mockResolvedValue({
570
- data: [firstEntry, downtimeEntry],
571
- nextPageToken: undefined,
572
- });
573
- const second = createSyncHarness(reloadedObjects, { collectionMetadata });
574
- mockState.subscribeCallback?.({ type: "state", status: "open" });
575
- await vi.waitFor(() => {
576
- expect(second.syncedData.get(2)).toMatchObject({ name: "Employee Two" });
577
- });
578
- expect(second.writes).toEqual([{ type: "insert", primaryKey: 2 }]);
579
- expect(mockState.getEditsHistory.mock.calls[0]?.[3]).toMatchObject({
580
- filters: {
581
- type: "timestampFilter",
582
- startTime: timestamp,
583
- },
584
- });
585
- expect([...collectionMetadata.values()][0].seenEntryKeysAtTimestamp).toHaveLength(2);
586
- second.cleanup();
587
- });
588
- it("persists delete and cursor progress for unloaded rows", async () => {
589
- const timestamp = "2099-03-12T12:02:00.000Z";
590
- mockState.getEditsHistory.mockResolvedValue({
591
- data: [
592
- {
593
- objectPrimaryKey: { employeeId: 404 },
594
- operationId: "op-delete-missing",
595
- actionTypeRid: "action-1",
596
- userId: "user-1",
597
- timestamp,
598
- edit: {
599
- type: "deleteEdit",
600
- previousProperties: {},
601
- },
602
- },
603
- ],
604
- nextPageToken: undefined,
605
- });
606
- const harness = createSyncHarness();
607
- mockState.subscribeCallback?.({ type: "state", status: "open" });
608
- await vi.waitFor(() => {
609
- expect(harness.transactions).toHaveLength(2);
610
- });
611
- expect(harness.writes).toEqual([
612
- {
613
- type: "delete",
614
- primaryKey: 404,
615
- },
616
- ]);
617
- expect(harness.transactions.at(-1)?.writes).toEqual([
618
- {
619
- type: "delete",
620
- primaryKey: 404,
621
- },
622
- ]);
623
- expect(harness.transactions.at(-1)?.collectionMetadata.size).toBe(1);
624
- expect([...harness.collectionMetadata.values()][0]).toMatchObject({
625
- version: 1,
626
- timestamp,
627
- });
628
- harness.cleanup();
629
- });
630
- it("decodes create edits with null property sentinels from edit history", async () => {
631
- mockState.getEditsHistory.mockResolvedValue({
632
- data: [
633
- {
634
- objectPrimaryKey: { employeeId: 3 },
635
- operationId: "op-1",
636
- actionTypeRid: "action-1",
637
- userId: "user-1",
638
- timestamp: "2099-03-12T12:00:00.000Z",
639
- edit: {
640
- type: "createEdit",
641
- properties: {
642
- employeeId: 3,
643
- nickname: "NullPropertyValue{}",
644
- name: "Employee Three",
645
- createdAt: "2099-03-12T12:00:00.000Z",
646
- },
647
- },
648
- },
649
- ],
650
- nextPageToken: undefined,
651
- });
652
- const codec = createFoundryCodec({
653
- types: [],
654
- objectTypes: [
655
- {
656
- name: "Employee",
657
- displayName: "Employee",
658
- pluralDisplayName: "Employees",
659
- primaryKey: "employeeId",
660
- properties: [
661
- { name: "employeeId", displayName: "Employee ID", type: o.integer({}) },
662
- { name: "name", displayName: "Name", type: o.string({}) },
663
- {
664
- name: "nickname",
665
- displayName: "Nickname",
666
- type: o.optional({ type: o.string({}) }),
667
- },
668
- ],
669
- },
670
- ],
671
- linkTypes: [],
672
- actionTypes: [],
673
- queryFunctionTypes: [],
674
- });
675
- const harness = createSyncHarness([], {
676
- decodeEditObject: (object) => codec.decodeEditObject("Employee", object),
677
- });
678
- mockState.subscribeCallback?.({ type: "state", status: "open" });
679
- await vi.waitFor(() => {
680
- expect(harness.syncedData.get(3)).toEqual({
681
- employeeId: 3,
682
- name: "Employee Three",
683
- nickname: undefined,
684
- createdAt: "2099-03-12T12:00:00.000Z",
685
- });
686
- });
687
- expect(mockState.getObject).not.toHaveBeenCalled();
688
- harness.cleanup();
689
- });
690
- it("decodes attachment property wrappers from edit history", async () => {
691
- mockState.getEditsHistory.mockResolvedValue({
692
- data: [
693
- {
694
- objectPrimaryKey: { employeeId: 7 },
695
- operationId: "op-7",
696
- actionTypeRid: "action-1",
697
- userId: "user-1",
698
- timestamp: "2099-03-12T12:00:00.000Z",
699
- edit: {
700
- type: "createEdit",
701
- properties: {
702
- employeeId: 7,
703
- attachments: [
704
- {
705
- type: "attachment",
706
- attachment: "ri.attachments.main.attachment.7",
707
- },
708
- ],
709
- name: "Employee Seven",
710
- },
711
- },
712
- },
713
- ],
714
- nextPageToken: undefined,
715
- });
716
- const codec = createFoundryCodec({
717
- types: [],
718
- objectTypes: [
719
- {
720
- name: "Employee",
721
- displayName: "Employee",
722
- pluralDisplayName: "Employees",
723
- primaryKey: "employeeId",
724
- properties: [
725
- { name: "employeeId", displayName: "Employee ID", type: o.integer({}) },
726
- { name: "name", displayName: "Name", type: o.string({}) },
727
- {
728
- name: "attachments",
729
- displayName: "Attachments",
730
- type: o.list({
731
- elementType: o.attachment({
732
- meta: {
733
- foundry: {
734
- kind: "attachment",
735
- },
736
- },
737
- }),
738
- }),
739
- },
740
- ],
741
- },
742
- ],
743
- linkTypes: [],
744
- actionTypes: [],
745
- queryFunctionTypes: [],
746
- });
747
- const harness = createSyncHarness([], {
748
- decodeObject: (object) => codec.decodeObject("Employee", object),
749
- decodeEditObject: (object) => codec.decodeEditObject("Employee", object),
750
- });
751
- mockState.subscribeCallback?.({ type: "state", status: "open" });
752
- await vi.waitFor(() => {
753
- expect(harness.syncedData.get(7)).toEqual({
754
- employeeId: 7,
755
- name: "Employee Seven",
756
- attachments: [{ id: "ri.attachments.main.attachment.7" }],
757
- });
758
- });
759
- harness.cleanup();
760
- });
761
- it("decodes media-reference wrappers from edit history using the property schema", async () => {
762
- const mediaReference = {
763
- mimeType: "image/png",
764
- reference: {
765
- type: "mediaSetViewItem",
766
- mediaSetViewItem: {
767
- mediaSetRid: "ri.mio.main.media-set.1",
768
- mediaSetViewRid: "ri.mio.main.view.1",
769
- mediaItemRid: "ri.mio.main.media-item.1",
770
- },
771
- },
772
- };
773
- mockState.getEditsHistory.mockResolvedValue({
774
- data: [
775
- {
776
- objectPrimaryKey: { employeeId: 8 },
777
- operationId: "op-8",
778
- actionTypeRid: "action-1",
779
- userId: "user-1",
780
- timestamp: "2099-03-12T12:00:00.000Z",
781
- edit: {
782
- type: "createEdit",
783
- properties: {
784
- employeeId: 8,
785
- logoMedia: {
786
- type: "mediaReference",
787
- mediaReference,
788
- },
789
- },
790
- },
791
- },
792
- ],
793
- nextPageToken: undefined,
794
- });
795
- const codec = createFoundryCodec({
796
- types: [],
797
- objectTypes: [
798
- {
799
- name: "Employee",
800
- displayName: "Employee",
801
- pluralDisplayName: "Employees",
802
- primaryKey: "employeeId",
803
- properties: [
804
- { name: "employeeId", displayName: "Employee ID", type: o.integer({}) },
805
- {
806
- name: "logoMedia",
807
- displayName: "Logo media",
808
- type: o.optional({
809
- type: o.attachment({
810
- meta: {
811
- foundry: {
812
- kind: "media",
813
- },
814
- },
815
- }),
816
- }),
817
- },
818
- ],
819
- },
820
- ],
821
- linkTypes: [],
822
- actionTypes: [],
823
- queryFunctionTypes: [],
824
- });
825
- const harness = createSyncHarness([], {
826
- decodeEditObject: (object) => codec.decodeEditObject("Employee", object),
827
- });
828
- mockState.subscribeCallback?.({ type: "state", status: "open" });
829
- await vi.waitFor(() => {
830
- expect(harness.syncedData.get(8)).toEqual({
831
- employeeId: 8,
832
- logoMedia: {
833
- id: [
834
- "ri.mio.main.media-set.1",
835
- "ri.mio.main.view.1",
836
- "ri.mio.main.media-item.1",
837
- ].join(":"),
838
- type: "image/png",
839
- },
840
- });
841
- });
842
- harness.cleanup();
843
- });
844
- it("decodes GeoPointPropertyValue strings from edit history", async () => {
845
- mockState.getEditsHistory.mockResolvedValue({
846
- data: [
847
- {
848
- objectPrimaryKey: { employeeId: 5 },
849
- operationId: "op-1",
850
- actionTypeRid: "action-1",
851
- userId: "user-1",
852
- timestamp: "2099-03-12T12:00:00.000Z",
853
- edit: {
854
- type: "createEdit",
855
- properties: {
856
- employeeId: 5,
857
- name: "Employee Five",
858
- location: "GeoPointPropertyValue{latitude: 40.375786, longitude: -74.11144}",
859
- },
860
- },
861
- },
862
- ],
863
- nextPageToken: undefined,
864
- });
865
- const codec = createFoundryCodec({
866
- types: [],
867
- objectTypes: [
868
- {
869
- name: "Employee",
870
- displayName: "Employee",
871
- pluralDisplayName: "Employees",
872
- primaryKey: "employeeId",
873
- properties: [
874
- { name: "employeeId", displayName: "Employee ID", type: o.integer({}) },
875
- { name: "name", displayName: "Name", type: o.string({}) },
876
- { name: "location", displayName: "Location", type: o.geopoint({}) },
877
- ],
878
- },
879
- ],
880
- linkTypes: [],
881
- actionTypes: [],
882
- queryFunctionTypes: [],
883
- });
884
- const harness = createSyncHarness([], {
885
- decodeEditObject: (object) => codec.decodeEditObject("Employee", object),
886
- });
887
- mockState.subscribeCallback?.({ type: "state", status: "open" });
888
- await vi.waitFor(() => {
889
- expect(harness.syncedData.get(5)).toEqual({
890
- employeeId: 5,
891
- name: "Employee Five",
892
- location: { lat: 40.375786, lon: -74.11144 },
893
- });
894
- });
895
- expect(mockState.getObject).not.toHaveBeenCalled();
896
- harness.cleanup();
897
- });
898
- it("unwraps tagged primary key values from edit history", async () => {
899
- mockState.getEditsHistory.mockResolvedValue({
900
- data: [
901
- {
902
- objectPrimaryKey: {
903
- employeeId: {
904
- type: "stringValue",
905
- value: "employee-4",
906
- },
907
- },
908
- operationId: "op-1",
909
- actionTypeRid: "action-1",
910
- userId: "user-1",
911
- timestamp: "2099-03-12T12:00:00.000Z",
912
- edit: {
913
- type: "createEdit",
914
- properties: {
915
- employeeId: {
916
- type: "stringValue",
917
- value: "employee-4",
918
- },
919
- name: "Employee Four",
920
- },
921
- },
922
- },
923
- ],
924
- nextPageToken: undefined,
925
- });
926
- const codec = createFoundryCodec({
927
- types: [],
928
- objectTypes: [
929
- {
930
- name: "Employee",
931
- displayName: "Employee",
932
- pluralDisplayName: "Employees",
933
- primaryKey: "employeeId",
934
- properties: [
935
- { name: "employeeId", displayName: "Employee ID", type: o.string({}) },
936
- { name: "name", displayName: "Name", type: o.string({}) },
937
- ],
938
- },
939
- ],
940
- linkTypes: [],
941
- actionTypes: [],
942
- queryFunctionTypes: [],
943
- });
944
- const harness = createSyncHarness([], {
945
- decodeEditObject: (object) => codec.decodeEditObject("Employee", object),
946
- });
947
- mockState.subscribeCallback?.({ type: "state", status: "open" });
948
- await vi.waitFor(() => {
949
- expect(harness.syncedData.get("employee-4")).toEqual({
950
- employeeId: "employee-4",
951
- name: "Employee Four",
952
- });
953
- });
954
- expect(mockState.getObject).not.toHaveBeenCalled();
955
- harness.cleanup();
956
- });
957
- it("triggers history catch-up on ADDED_OR_UPDATED watcher events", async () => {
958
- mockState.getEditsHistory
959
- .mockResolvedValueOnce({
960
- data: [],
961
- nextPageToken: undefined,
962
- })
963
- .mockResolvedValueOnce({
964
- data: [
965
- {
966
- objectPrimaryKey: { employeeId: 10 },
967
- operationId: "op-10",
968
- actionTypeRid: "action-1",
969
- userId: "user-1",
970
- timestamp: "2099-03-12T12:05:00.000Z",
971
- edit: {
972
- type: "createEdit",
973
- properties: {
974
- employeeId: 10,
975
- name: "New Employee",
976
- },
977
- },
978
- },
979
- ],
980
- nextPageToken: undefined,
981
- });
982
- const harness = createSyncHarness();
983
- mockState.subscribeCallback?.({ type: "state", status: "open" });
984
- await vi.waitFor(() => {
985
- expect(mockState.getEditsHistory).toHaveBeenCalledTimes(1);
986
- });
987
- mockState.subscribeCallback?.({
988
- type: "change",
989
- updates: [
990
- {
991
- type: "object",
992
- state: "ADDED_OR_UPDATED",
993
- object: { employeeId: 10, name: "New Employee" },
994
- },
995
- ],
996
- });
997
- await vi.waitFor(() => {
998
- expect(mockState.getEditsHistory).toHaveBeenCalledTimes(2);
999
- expect(harness.syncedData.get(10)).toEqual({
1000
- employeeId: 10,
1001
- name: "New Employee",
1002
- });
1003
- });
1004
- harness.cleanup();
1005
- });
1006
- it("falls back to direct websocket updates when edit history is unavailable", async () => {
1007
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
1008
- mockState.getEditsHistory.mockRejectedValue(new Error("Edit history is not enabled"));
1009
- const harness = createSyncHarness();
1010
- const baselineCheckpoint = JSON.parse(JSON.stringify([...harness.collectionMetadata.values()][0]));
1011
- mockState.subscribeCallback?.({
1012
- type: "change",
1013
- updates: [
1014
- {
1015
- type: "object",
1016
- state: "ADDED_OR_UPDATED",
1017
- object: { employeeId: 12, name: "Direct Employee" },
1018
- },
1019
- ],
1020
- });
1021
- await vi.waitFor(() => {
1022
- expect(mockState.getEditsHistory).toHaveBeenCalledTimes(1);
1023
- expect(harness.syncedData.get(12)).toEqual({
1024
- employeeId: 12,
1025
- name: "Direct Employee",
1026
- });
1027
- });
1028
- mockState.subscribeCallback?.({
1029
- type: "change",
1030
- updates: [
1031
- {
1032
- type: "object",
1033
- state: "REMOVED",
1034
- object: { employeeId: 12 },
1035
- },
1036
- ],
1037
- });
1038
- await vi.waitFor(() => {
1039
- expect(harness.syncedData.has(12)).toBe(false);
1040
- expect(harness.writes).toContainEqual({ type: "delete", primaryKey: 12 });
1041
- });
1042
- expect(mockState.getEditsHistory).toHaveBeenCalledTimes(1);
1043
- expect([...harness.collectionMetadata.values()][0]).toEqual(baselineCheckpoint);
1044
- expect(harness.transactions.at(-1)?.collectionMetadata.size).toBe(0);
1045
- harness.cleanup();
1046
- warnSpy.mockRestore();
1047
- });
1048
- it("fully reconciles after websocket reconnect when edit history is unavailable", async () => {
1049
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
1050
- mockState.getEditsHistory.mockRejectedValue(new Error("Edit history is not enabled"));
1051
- mockState.search.mockResolvedValue({
1052
- data: [],
1053
- nextPageToken: undefined,
1054
- });
1055
- const harness = createSyncHarness();
1056
- mockState.subscribeCallback?.({
1057
- type: "state",
1058
- status: "open",
1059
- });
1060
- await vi.waitFor(() => {
1061
- expect(mockState.search).toHaveBeenCalledOnce();
1062
- });
1063
- // Simulate stale persisted state becoming visible while the watcher is
1064
- // disconnected and therefore unable to report its deletion.
1065
- harness.syncedData.set(30, {
1066
- employeeId: 30,
1067
- name: "Stale Employee",
1068
- });
1069
- mockState.subscribeCallback?.({
1070
- type: "state",
1071
- status: "open",
1072
- });
1073
- await vi.waitFor(() => {
1074
- expect(mockState.search).toHaveBeenCalledTimes(2);
1075
- expect(harness.syncedData.has(30)).toBe(false);
1076
- });
1077
- expect(harness.truncate).toHaveBeenCalled();
1078
- harness.cleanup();
1079
- warnSpy.mockRestore();
1080
- });
1081
- it("does not checkpoint partially fetched history when falling back", async () => {
1082
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
1083
- mockState.getEditsHistory
1084
- .mockResolvedValueOnce({
1085
- data: [
1086
- {
1087
- objectPrimaryKey: { employeeId: 20 },
1088
- operationId: "op-history-20",
1089
- actionTypeRid: "action-1",
1090
- userId: "user-1",
1091
- timestamp: "2099-03-12T12:10:00.000Z",
1092
- edit: {
1093
- type: "createEdit",
1094
- properties: {
1095
- employeeId: 20,
1096
- name: "Uncommitted History Employee",
1097
- },
1098
- },
1099
- },
1100
- ],
1101
- nextPageToken: "next",
1102
- })
1103
- .mockRejectedValueOnce(new Error("Edit history pagination failed"));
1104
- const harness = createSyncHarness();
1105
- const baselineCheckpoint = JSON.parse(JSON.stringify([...harness.collectionMetadata.values()][0]));
1106
- mockState.subscribeCallback?.({
1107
- type: "change",
1108
- updates: [
1109
- {
1110
- type: "object",
1111
- state: "ADDED_OR_UPDATED",
1112
- object: { employeeId: 21, name: "Direct Employee" },
1113
- },
1114
- ],
1115
- });
1116
- await vi.waitFor(() => {
1117
- expect(mockState.getEditsHistory).toHaveBeenCalledTimes(2);
1118
- expect(harness.syncedData.get(21)).toMatchObject({ name: "Direct Employee" });
1119
- });
1120
- expect(harness.syncedData.has(20)).toBe(false);
1121
- expect(harness.writes).toEqual([{ type: "insert", primaryKey: 21 }]);
1122
- expect([...harness.collectionMetadata.values()][0]).toEqual(baselineCheckpoint);
1123
- harness.cleanup();
1124
- warnSpy.mockRestore();
1125
- });
1126
- it("returns immediately when awaiting an operation before sync has started", async () => {
1127
- const { utils } = objectCollectionOptions({
1128
- client: {
1129
- baseUrl: "https://example.com",
1130
- fetch: vi.fn(),
1131
- ontologyRid: "ri.ontology.main",
1132
- tokenProvider: () => Promise.resolve("token"),
1133
- },
1134
- objectType: "Employee",
1135
- primaryKeyProperty: "employeeId",
1136
- selectedProperties: ["employeeId", "name"],
1137
- });
1138
- await expect(utils.awaitOperationId("op-before-sync")).resolves.toBeUndefined();
1139
- expect(mockState.getEditsHistory).not.toHaveBeenCalled();
1140
- });
1141
- it("resolves awaitOperationId after direct websocket sync observes an update", async () => {
1142
- const warnSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
1143
- mockState.getEditsHistory.mockRejectedValue(new Error("Edit history is not enabled"));
1144
- let resolveCommit;
1145
- const commitApplied = new Promise((resolve) => {
1146
- resolveCommit = resolve;
1147
- });
1148
- const harness = createSyncHarness([], {
1149
- commitReceipt: () => commitApplied,
1150
- });
1151
- const operationPromise = harness.utils.awaitOperationId("op-12");
1152
- await vi.waitFor(() => {
1153
- expect(mockState.getEditsHistory).toHaveBeenCalledTimes(1);
1154
- });
1155
- mockState.subscribeCallback?.({
1156
- type: "change",
1157
- updates: [
1158
- {
1159
- type: "object",
1160
- state: "ADDED_OR_UPDATED",
1161
- object: { employeeId: 13, name: "Direct Operation Employee" },
1162
- },
1163
- ],
1164
- });
1165
- await expect(operationPromise).resolves.toBeUndefined();
1166
- expect(harness.syncedData.get(13)).toEqual({
1167
- employeeId: 13,
1168
- name: "Direct Operation Employee",
1169
- });
1170
- expect(mockState.getEditsHistory).toHaveBeenCalledTimes(1);
1171
- resolveCommit();
1172
- await commitApplied;
1173
- harness.cleanup();
1174
- warnSpy.mockRestore();
1175
- });
1176
- it("awaitOperationId resolves after the matching edit is observed", async () => {
1177
- mockState.getEditsHistory.mockResolvedValue({
1178
- data: [
1179
- {
1180
- objectPrimaryKey: { employeeId: 11 },
1181
- operationId: "op-11",
1182
- actionTypeRid: "action-1",
1183
- userId: "user-1",
1184
- timestamp: "2099-03-12T12:06:00.000Z",
1185
- edit: {
1186
- type: "createEdit",
1187
- properties: {
1188
- employeeId: 11,
1189
- name: "Operation Eleven",
1190
- },
1191
- },
1192
- },
1193
- ],
1194
- nextPageToken: undefined,
1195
- });
1196
- let resolveCommit;
1197
- const commitApplied = new Promise((resolve) => {
1198
- resolveCommit = resolve;
1199
- });
1200
- const harness = createSyncHarness([], {
1201
- commitReceipt: () => commitApplied,
1202
- });
1203
- await expect(harness.utils.awaitOperationId("op-11")).resolves.toBeUndefined();
1204
- expect(mockState.getEditsHistory).toHaveBeenCalledTimes(1);
1205
- expect(harness.syncedData.get(11)).toEqual({
1206
- employeeId: 11,
1207
- name: "Operation Eleven",
1208
- });
1209
- resolveCommit();
1210
- await commitApplied;
1211
- harness.cleanup();
1212
- });
1213
- it("combines the subset where clause with cursor.whereFrom", async () => {
1214
- mockState.search.mockResolvedValue({
1215
- data: [{ employeeId: 2, status: "open", priority: 6 }],
1216
- nextPageToken: undefined,
1217
- });
1218
- const harness = createSyncHarness();
1219
- await harness.loadSubset({
1220
- where: eq(new IR.PropRef(["status"]), "open"),
1221
- cursor: {
1222
- whereFrom: gt(new IR.PropRef(["priority"]), 5),
1223
- whereCurrent: eq(new IR.PropRef(["priority"]), 5),
1224
- },
1225
- });
1226
- const searchOptions = mockState.search.mock.calls[0]?.[3];
1227
- expect(searchOptions).toMatchObject({
1228
- where: {
1229
- type: "and",
1230
- value: [
1231
- {
1232
- type: "eq",
1233
- propertyIdentifier: { type: "property", apiName: "status" },
1234
- value: "open",
1235
- },
1236
- {
1237
- type: "gt",
1238
- propertyIdentifier: { type: "property", apiName: "priority" },
1239
- value: 5,
1240
- },
1241
- ],
1242
- },
1243
- });
1244
- expect(searchOptions).toMatchObject({
1245
- select: [
1246
- "employeeId",
1247
- "name",
1248
- "status",
1249
- "priority",
1250
- ],
1251
- });
1252
- expect(searchOptions?.selectV2).toEqual([]);
1253
- expect(harness.syncedData.has(2)).toBe(true);
1254
- harness.cleanup();
1255
- });
1256
- it("over-fetches ordered subsets before applying an offset", async () => {
1257
- mockState.search.mockResolvedValue({
1258
- data: [
1259
- { employeeId: 1, priority: 1 },
1260
- { employeeId: 2, priority: 2 },
1261
- { employeeId: 3, priority: 3 },
1262
- { employeeId: 4, priority: 4 },
1263
- ],
1264
- nextPageToken: undefined,
1265
- });
1266
- const harness = createSyncHarness();
1267
- await harness.loadSubset({
1268
- orderBy: [
1269
- {
1270
- expression: new IR.PropRef(["priority"]),
1271
- compareOptions: {
1272
- direction: "asc",
1273
- nulls: "last",
1274
- },
1275
- },
1276
- ],
1277
- offset: 2,
1278
- limit: 2,
1279
- });
1280
- expect(mockState.search.mock.calls[0]?.[3]).toMatchObject({
1281
- pageSize: 4,
1282
- orderBy: {
1283
- fields: [{ field: "priority", direction: "asc" }],
1284
- },
1285
- });
1286
- expect([...harness.syncedData.keys()]).toEqual([3, 4]);
1287
- harness.cleanup();
1288
- });
1289
- });
1290
- //# sourceMappingURL=objectCollectionOptions.test.js.map