@seedprotocol/sdk 0.4.3 → 0.4.5

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 (199) hide show
  1. package/README.md +38 -348
  2. package/dist/{ArweaveClient-CleX_4Gw.js → ArweaveClient-CgWK-JgT.js} +8 -8
  3. package/dist/{ArweaveClient-CleX_4Gw.js.map → ArweaveClient-CgWK-JgT.js.map} +1 -1
  4. package/dist/{ArweaveClient-BvJ1FhQ5.js → ArweaveClient-WcG8CZAE.js} +8 -8
  5. package/dist/{ArweaveClient-BvJ1FhQ5.js.map → ArweaveClient-WcG8CZAE.js.map} +1 -1
  6. package/dist/{Db-DX08SxS9.js → Db-DjFdIdR9.js} +9 -16
  7. package/dist/{Db-DX08SxS9.js.map → Db-DjFdIdR9.js.map} +1 -1
  8. package/dist/{Db-BPnO1-_p.js → Db-DjofXdeU.js} +9 -9
  9. package/dist/{Db-BPnO1-_p.js.map → Db-DjofXdeU.js.map} +1 -1
  10. package/dist/{EasClient-BwhUcPjY.js → EasClient-Aojewp6P.js} +8 -8
  11. package/dist/{EasClient-CJSs38Db.js.map → EasClient-Aojewp6P.js.map} +1 -1
  12. package/dist/{EasClient-CJSs38Db.js → EasClient-BVFXp2O6.js} +8 -8
  13. package/dist/{EasClient-BwhUcPjY.js.map → EasClient-BVFXp2O6.js.map} +1 -1
  14. package/dist/{FileManager-B1tdLMsX.js → FileManager-C9zr4AJe.js} +8 -8
  15. package/dist/{FileManager-B1tdLMsX.js.map → FileManager-C9zr4AJe.js.map} +1 -1
  16. package/dist/{FileManager-Ct91ZhOE.js → FileManager-CxGJLw5C.js} +8 -8
  17. package/dist/{FileManager-Ct91ZhOE.js.map → FileManager-CxGJLw5C.js.map} +1 -1
  18. package/dist/Item/Item.d.ts +28 -7
  19. package/dist/Item/Item.d.ts.map +1 -1
  20. package/dist/Item/service/actors/runPublish.d.ts +5 -0
  21. package/dist/Item/service/actors/runPublish.d.ts.map +1 -0
  22. package/dist/Item/service/itemMachineSingle.d.ts +10 -5
  23. package/dist/Item/service/itemMachineSingle.d.ts.map +1 -1
  24. package/dist/ItemProperty/ItemProperty.d.ts +30 -5
  25. package/dist/ItemProperty/ItemProperty.d.ts.map +1 -1
  26. package/dist/ItemProperty/service/actors/loadOrCreateProperty.d.ts.map +1 -1
  27. package/dist/ItemProperty/service/propertyMachine.d.ts +10 -10
  28. package/dist/ItemProperty/service/propertyMachine.d.ts.map +1 -1
  29. package/dist/Model/Model.d.ts +27 -20
  30. package/dist/Model/Model.d.ts.map +1 -1
  31. package/dist/Model/index.d.ts +1 -1
  32. package/dist/Model/service/actors/createModelProperties.d.ts.map +1 -1
  33. package/dist/Model/service/actors/loadOrCreateModel.d.ts.map +1 -1
  34. package/dist/Model/service/actors/validateModel.d.ts.map +1 -1
  35. package/dist/Model/service/modelMachine.d.ts +18 -3
  36. package/dist/Model/service/modelMachine.d.ts.map +1 -1
  37. package/dist/ModelProperty/ModelProperty.d.ts +25 -2
  38. package/dist/ModelProperty/ModelProperty.d.ts.map +1 -1
  39. package/dist/ModelProperty/service/actors/compareAndMarkDraft.d.ts.map +1 -1
  40. package/dist/ModelProperty/service/actors/saveToSchema.d.ts.map +1 -1
  41. package/dist/ModelProperty/service/actors/validateProperty.d.ts.map +1 -1
  42. package/dist/ModelProperty/service/modelPropertyMachine.d.ts +17 -3
  43. package/dist/ModelProperty/service/modelPropertyMachine.d.ts.map +1 -1
  44. package/dist/{ModelProperty-Cr3BmgkC.js → ModelProperty-CGdkocQ8.js} +349 -817
  45. package/dist/ModelProperty-CGdkocQ8.js.map +1 -0
  46. package/dist/{PathResolver-DJdxE_OK.js → PathResolver-CX6GHoTS.js} +8 -8
  47. package/dist/{PathResolver-DJdxE_OK.js.map → PathResolver-CX6GHoTS.js.map} +1 -1
  48. package/dist/{PathResolver-BErmcZqP.js → PathResolver-z_WX47_o.js} +8 -8
  49. package/dist/{PathResolver-BErmcZqP.js.map → PathResolver-z_WX47_o.js.map} +1 -1
  50. package/dist/{QueryClient-DIu9c-w6.js → QueryClient-ByKPdRmE.js} +8 -8
  51. package/dist/{QueryClient-DIu9c-w6.js.map → QueryClient-ByKPdRmE.js.map} +1 -1
  52. package/dist/{QueryClient-D2mv63gP.js → QueryClient-Cb1iJO-x.js} +8 -8
  53. package/dist/{QueryClient-D2mv63gP.js.map → QueryClient-Cb1iJO-x.js.map} +1 -1
  54. package/dist/Schema/Schema.d.ts +24 -3
  55. package/dist/Schema/Schema.d.ts.map +1 -1
  56. package/dist/Schema/service/actors/checkExistingSchema.d.ts.map +1 -1
  57. package/dist/Schema/service/actors/createPropertyInstances.d.ts.map +1 -1
  58. package/dist/Schema/service/actors/loadOrCreateSchema.d.ts.map +1 -1
  59. package/dist/Schema/service/actors/verifyPropertyInstancesInCache.d.ts.map +1 -1
  60. package/dist/Schema/service/actors/writeModelsToDb.d.ts.map +1 -1
  61. package/dist/Schema/service/actors/writePropertiesToDb.d.ts.map +1 -1
  62. package/dist/Schema/service/actors/writeSchemaToDb.d.ts.map +1 -1
  63. package/dist/Schema/service/addModelsMachine.d.ts.map +1 -1
  64. package/dist/Schema/service/schemaMachine.d.ts +17 -3
  65. package/dist/Schema/service/schemaMachine.d.ts.map +1 -1
  66. package/dist/{Schema-DeKabJ0T.js → Schema-D1eqDHyt.js} +995 -186
  67. package/dist/Schema-D1eqDHyt.js.map +1 -0
  68. package/dist/{SchemaValidationService-cTlURuDt.js → SchemaValidationService-DyttFaV_.js} +7 -7
  69. package/dist/{SchemaValidationService-cTlURuDt.js.map → SchemaValidationService-DyttFaV_.js.map} +1 -1
  70. package/dist/browser/db/Db.d.ts.map +1 -1
  71. package/dist/browser/react/SeedProvider.d.ts +30 -0
  72. package/dist/browser/react/SeedProvider.d.ts.map +1 -0
  73. package/dist/browser/react/index.d.ts +4 -1
  74. package/dist/browser/react/index.d.ts.map +1 -1
  75. package/dist/browser/react/item.d.ts +10 -6
  76. package/dist/browser/react/item.d.ts.map +1 -1
  77. package/dist/browser/react/itemProperty.d.ts +37 -1
  78. package/dist/browser/react/itemProperty.d.ts.map +1 -1
  79. package/dist/browser/react/liveQuery.d.ts.map +1 -1
  80. package/dist/browser/react/model.d.ts +21 -7
  81. package/dist/browser/react/model.d.ts.map +1 -1
  82. package/dist/browser/react/modelProperty.d.ts +23 -0
  83. package/dist/browser/react/modelProperty.d.ts.map +1 -1
  84. package/dist/browser/react/queryClient.d.ts +28 -0
  85. package/dist/browser/react/queryClient.d.ts.map +1 -0
  86. package/dist/browser/react/schema.d.ts +8 -0
  87. package/dist/browser/react/schema.d.ts.map +1 -1
  88. package/dist/browser/react/trash.d.ts +5 -2
  89. package/dist/browser/react/trash.d.ts.map +1 -1
  90. package/dist/cjs/{ModelProperty-MkN5Rmx7.js → ModelProperty-BeJvgKMw.js} +377 -477
  91. package/dist/cjs/ModelProperty-BeJvgKMw.js.map +1 -0
  92. package/dist/cjs/{Schema-B5cr_JVK.js → Schema-CVs9J6eP.js} +709 -263
  93. package/dist/cjs/Schema-CVs9J6eP.js.map +1 -0
  94. package/dist/cjs/{SchemaValidationService-BgIzc3-r.js → SchemaValidationService-CDKcVRFQ.js} +4 -4
  95. package/dist/cjs/{SchemaValidationService-BgIzc3-r.js.map → SchemaValidationService-CDKcVRFQ.js.map} +1 -1
  96. package/dist/cjs/{getItem-CVJJPky2.js → getItem-B5RYPvrG.js} +4 -4
  97. package/dist/cjs/{getItem-CVJJPky2.js.map → getItem-B5RYPvrG.js.map} +1 -1
  98. package/dist/cjs/{getPublishPayload-DbOc3WA-.js → getPublishPayload-BD1qRob1.js} +26 -11
  99. package/dist/cjs/getPublishPayload-BD1qRob1.js.map +1 -0
  100. package/dist/cjs/{getPublishUploads-NzioLz-3.js → getPublishUploads-CnC9aYxs.js} +5 -5
  101. package/dist/cjs/getPublishUploads-CnC9aYxs.js.map +1 -0
  102. package/dist/cjs/{getSegmentedItemProperties-BsaklLwI.js → getSegmentedItemProperties-B_njnntx.js} +2 -2
  103. package/dist/cjs/{getSegmentedItemProperties-BsaklLwI.js.map → getSegmentedItemProperties-B_njnntx.js.map} +1 -1
  104. package/dist/cjs/{index-BmIVfqGN.js → index-BeKPbbk0.js} +12715 -12384
  105. package/dist/cjs/index-BeKPbbk0.js.map +1 -0
  106. package/dist/cjs/{index-C_0angRB.js → index-Dnywap_P.js} +4 -4
  107. package/dist/cjs/index-Dnywap_P.js.map +1 -0
  108. package/dist/client/actors/platformClassesInit.d.ts.map +1 -1
  109. package/dist/client/actors/processSchemaFiles.d.ts.map +1 -1
  110. package/dist/client/actors/saveAppState.d.ts.map +1 -1
  111. package/dist/db/read/getItemData.d.ts.map +1 -1
  112. package/dist/db/read/getItems.d.ts.map +1 -1
  113. package/dist/db/read/getModelPropertiesData.d.ts +19 -0
  114. package/dist/db/read/getModelPropertiesData.d.ts.map +1 -0
  115. package/dist/db/read/getModelsData.d.ts +15 -0
  116. package/dist/db/read/getModelsData.d.ts.map +1 -0
  117. package/dist/db/read/getPublishPayload.d.ts.map +1 -1
  118. package/dist/db/read/getPublishUploads.d.ts +1 -7
  119. package/dist/db/read/getPublishUploads.d.ts.map +1 -1
  120. package/dist/db/read/getSchemaUidForModel.d.ts.map +1 -1
  121. package/dist/db/write/updateSeedUid.d.ts +7 -0
  122. package/dist/db/write/updateSeedUid.d.ts.map +1 -0
  123. package/dist/eas.d.ts.map +1 -1
  124. package/dist/events/item/index.d.ts.map +1 -1
  125. package/dist/events/item/syncDbWithEas.d.ts.map +1 -1
  126. package/dist/{getItem-CcttmUY_.js → getItem-BB5HBCbK.js} +8 -8
  127. package/dist/{getItem-CcttmUY_.js.map → getItem-BB5HBCbK.js.map} +1 -1
  128. package/dist/{getPublishPayload-NFpqbd_H.js → getPublishPayload-uLm0AqN_.js} +29 -14
  129. package/dist/getPublishPayload-uLm0AqN_.js.map +1 -0
  130. package/dist/{getPublishUploads-Cpb9vgwE.js → getPublishUploads-Dc-HqhO8.js} +9 -9
  131. package/dist/getPublishUploads-Dc-HqhO8.js.map +1 -0
  132. package/dist/{getSegmentedItemProperties-DiyQPMgI.js → getSegmentedItemProperties-BrIqFNfD.js} +2 -2
  133. package/dist/{getSegmentedItemProperties-DiyQPMgI.js.map → getSegmentedItemProperties-BrIqFNfD.js.map} +1 -1
  134. package/dist/helpers/db.d.ts +12 -0
  135. package/dist/helpers/db.d.ts.map +1 -1
  136. package/dist/helpers/entity/entityDestroy.d.ts +41 -0
  137. package/dist/helpers/entity/entityDestroy.d.ts.map +1 -0
  138. package/dist/helpers/entity/index.d.ts +1 -0
  139. package/dist/helpers/entity/index.d.ts.map +1 -1
  140. package/dist/helpers/index.d.ts +1 -0
  141. package/dist/helpers/index.d.ts.map +1 -1
  142. package/dist/helpers/property/index.d.ts +12 -12
  143. package/dist/helpers/property/index.d.ts.map +1 -1
  144. package/dist/helpers/reactiveProxy.d.ts.map +1 -1
  145. package/dist/helpers/schema.d.ts.map +1 -1
  146. package/dist/helpers/updateSchema.d.ts +9 -0
  147. package/dist/helpers/updateSchema.d.ts.map +1 -1
  148. package/dist/helpers/waitForEntityIdle.d.ts +2 -2
  149. package/dist/helpers/waitForEntityIdle.d.ts.map +1 -1
  150. package/dist/imports/json.d.ts.map +1 -1
  151. package/dist/{index-r45w9hEq.js → index-2FcQHgKp.js} +2 -2
  152. package/dist/index-2FcQHgKp.js.map +1 -0
  153. package/dist/{json-I3vJhXo8.js → index-DPll6EAp.js} +12450 -12121
  154. package/dist/index-DPll6EAp.js.map +1 -0
  155. package/dist/{index-CRuq6HVi.js → index-LEY0Og1p.js} +9 -9
  156. package/dist/index-LEY0Og1p.js.map +1 -0
  157. package/dist/index.d.ts +3 -1
  158. package/dist/index.d.ts.map +1 -1
  159. package/dist/interfaces/IItem.d.ts +2 -0
  160. package/dist/interfaces/IItem.d.ts.map +1 -1
  161. package/dist/interfaces/IItemProperty.d.ts +1 -0
  162. package/dist/interfaces/IItemProperty.d.ts.map +1 -1
  163. package/dist/main.cjs +3 -3
  164. package/dist/main.js +999 -1033
  165. package/dist/main.js.map +1 -1
  166. package/dist/node.js +16 -16
  167. package/dist/node.js.map +1 -1
  168. package/dist/{property-Dy09KTxg.js → property-B15X7jLX.js} +7 -5
  169. package/dist/property-B15X7jLX.js.map +1 -0
  170. package/dist/{queries-LZYSuhtz.js → queries-BPDSpiEX.js} +2 -2
  171. package/dist/{queries-LZYSuhtz.js.map → queries-BPDSpiEX.js.map} +1 -1
  172. package/dist/services/write/actors/validateEntity.d.ts.map +1 -1
  173. package/dist/services/write/actors/writeToDatabase.d.ts.map +1 -1
  174. package/dist/services/write/writeProcessMachine.d.ts +1 -1
  175. package/dist/types/index.d.ts +9 -0
  176. package/dist/types/index.d.ts.map +1 -1
  177. package/dist/types/item.d.ts +12 -0
  178. package/dist/types/item.d.ts.map +1 -1
  179. package/dist/types/property.d.ts +6 -0
  180. package/dist/types/property.d.ts.map +1 -1
  181. package/dist/types/publish.d.ts +9 -0
  182. package/dist/types/publish.d.ts.map +1 -0
  183. package/package.json +12 -4
  184. package/dist/ModelProperty-Cr3BmgkC.js.map +0 -1
  185. package/dist/Schema-DeKabJ0T.js.map +0 -1
  186. package/dist/cjs/ModelProperty-MkN5Rmx7.js.map +0 -1
  187. package/dist/cjs/Schema-B5cr_JVK.js.map +0 -1
  188. package/dist/cjs/getPublishPayload-DbOc3WA-.js.map +0 -1
  189. package/dist/cjs/getPublishUploads-NzioLz-3.js.map +0 -1
  190. package/dist/cjs/index-BmIVfqGN.js.map +0 -1
  191. package/dist/cjs/index-C_0angRB.js.map +0 -1
  192. package/dist/events/item/publish.d.ts +0 -7
  193. package/dist/events/item/publish.d.ts.map +0 -1
  194. package/dist/getPublishPayload-NFpqbd_H.js.map +0 -1
  195. package/dist/getPublishUploads-Cpb9vgwE.js.map +0 -1
  196. package/dist/index-CRuq6HVi.js.map +0 -1
  197. package/dist/index-r45w9hEq.js.map +0 -1
  198. package/dist/json-I3vJhXo8.js.map +0 -1
  199. package/dist/property-Dy09KTxg.js.map +0 -1
package/dist/main.js CHANGED
@@ -1,29 +1,31 @@
1
1
  import 'reflect-metadata';
2
- import { ai as getClient, aj as ClientManagerState, p as BaseDb, X as seeds, ak as getVersionData, a0 as Item, al as createNewItem, C as metadata, a4 as ItemProperty, W as schemas, am as loadAllSchemasFromDb, q as models, G as modelSchemas, M as Model, r as properties, x as BaseEasClient, an as getSeedsBySchemaName, ao as GET_SEEDS, ap as getItemVersionsFromEas, aq as getItemPropertiesFromEas, z as BaseArweaveClient, ar as setSchemaUidForSchemaDefinition, as as parseEasRelationPropertyName } from './json-I3vJhXo8.js';
3
- export { s as FileManager, a as ModelPropertyDataTypes, aw as SeedModels, a6 as client, ay as createSchema, ax as getArweaveUrlForTransaction, at as getModelSchemasFromEas, aB as getSchemaNameFromId, au as getSchemaUidBySchemaName, av as getSeedsFromSchemaUids, a9 as importJsonSchema, aA as listSchemas, ab as readJsonImportFile, az as readSchema, ac as transformImportToSchemaFile } from './json-I3vJhXo8.js';
4
- import { g as getPropertySchema } from './property-Dy09KTxg.js';
5
- import { M as ModelProperty } from './ModelProperty-Cr3BmgkC.js';
6
- export { a as deleteModelFromSchema, d as deletePropertyFromModel, r as renameModelProperty, u as updateModelProperties } from './ModelProperty-Cr3BmgkC.js';
7
- import { Schema } from './Schema-DeKabJ0T.js';
8
- import { useState, useRef, useMemo, useEffect, useCallback } from 'react';
2
+ import { am as getClient, an as ClientManagerState, p as BaseDb, a0 as Item, X as seeds, ao as getVersionData, ap as createNewItem, C as metadata, a4 as ItemProperty, W as schemas, aq as loadAllSchemasFromDb, M as Model, q as models, G as modelSchemas, r as properties, a5 as eventEmitter, w as BaseEasClient, ar as getSeedsBySchemaName, as as GET_SEEDS, at as getItemVersionsFromEas, au as getItemPropertiesFromEas, u as BaseArweaveClient, av as setSchemaUidForSchemaDefinition, aw as parseEasRelationPropertyName } from './index-DPll6EAp.js';
3
+ export { x as FileManager, a as ModelPropertyDataTypes, aA as SeedModels, a6 as client, aC as createSchema, aB as getArweaveUrlForTransaction, ax as getModelSchemasFromEas, aF as getSchemaNameFromId, ay as getSchemaUidBySchemaName, az as getSeedsFromSchemaUids, a9 as importJsonSchema, aE as listSchemas, ab as readJsonImportFile, aD as readSchema, ac as transformImportToSchemaFile, af as waitForEntityIdle } from './index-DPll6EAp.js';
4
+ import { g as getPropertySchema } from './property-B15X7jLX.js';
5
+ import { ModelProperty } from './ModelProperty-CGdkocQ8.js';
6
+ import { S as Schema } from './Schema-D1eqDHyt.js';
7
+ export { a as deleteModelFromSchema, d as deletePropertyFromModel, r as renameModelProperty, u as updateModelProperties } from './Schema-D1eqDHyt.js';
8
+ import React, { useState, useRef, useMemo, useEffect, useCallback } from 'react';
9
+ import { flushSync } from 'react-dom';
9
10
  import { orderBy, startCase } from 'lodash-es';
10
11
  import debug from 'debug';
11
12
  import { useSelector } from '@xstate/react';
12
13
  import { eq, or, isNotNull, isNull, and, gt, desc } from 'drizzle-orm';
14
+ import { useQueryClient, useQuery, QueryClient as QueryClient$1, QueryClientProvider } from '@tanstack/react-query';
13
15
  import 'pluralize';
14
16
  import 'js-yaml';
15
17
  import 'fs';
16
18
  import 'xstate';
17
- import 'rxjs';
18
- import 'drizzle-orm/casing';
19
- import '@sinclair/typebox';
20
- import 'arweave';
21
- import 'eventemitter3';
22
19
  import 'drizzle-orm/sqlite-core';
23
- import 'ethers';
24
20
  import 'nanoid';
25
21
  import 'nanoid-dictionary';
26
- import './SchemaValidationService-cTlURuDt.js';
22
+ import 'ethers';
23
+ import 'rxjs';
24
+ import 'drizzle-orm/casing';
25
+ import 'eventemitter3';
26
+ import 'arweave';
27
+ import '@sinclair/typebox';
28
+ import './SchemaValidationService-DyttFaV_.js';
27
29
  import '@sinclair/typebox/value';
28
30
 
29
31
  const useIsClientReady = () => {
@@ -131,6 +133,7 @@ const useItem = ({ modelName, seedLocalId, seedUid }) => {
131
133
  const [isLoading, setIsLoading] = useState(!!(seedLocalId || seedUid));
132
134
  const [error, setError] = useState(null);
133
135
  const subscriptionRef = useRef(undefined);
136
+ const hasSeenIdleRef = useRef(false);
134
137
  const isClientReady = useIsClientReady();
135
138
  const modelNameRef = useRef(modelName);
136
139
  const seedLocalIdRef = useRef(seedLocalId);
@@ -163,7 +166,14 @@ const useItem = ({ modelName, seedLocalId, seedUid }) => {
163
166
  });
164
167
  if (!foundItem) {
165
168
  logger$2('[useItem] [loadItem] no item found', modelNameRef.current, seedLocalIdRef.current);
166
- setItem(undefined);
169
+ // Don't clear item if we already have one for the same request (e.g. duplicate loadItem from effect re-run)
170
+ setItem((prev) => {
171
+ if (!prev)
172
+ return undefined;
173
+ const match = (prev.seedLocalId && prev.seedLocalId === seedLocalIdRef.current) ||
174
+ (prev.seedUid && prev.seedUid === seedUidRef.current);
175
+ return match ? prev : undefined;
176
+ });
167
177
  setIsLoading(false);
168
178
  setError(null);
169
179
  return;
@@ -206,26 +216,33 @@ const useItem = ({ modelName, seedLocalId, seedUid }) => {
206
216
  // Clean up subscription if item is not available
207
217
  subscriptionRef.current?.unsubscribe();
208
218
  subscriptionRef.current = undefined;
219
+ hasSeenIdleRef.current = false;
209
220
  return;
210
221
  }
211
222
  // Clean up previous subscription
212
223
  subscriptionRef.current?.unsubscribe();
213
- // Subscribe to service changes
214
- // Don't set isLoading here - it's already set correctly in loadItem
215
- // Just subscribe to future state changes
224
+ hasSeenIdleRef.current = false;
225
+ // Subscribe to service changes. Only set isLoading to true after we've seen idle at least
226
+ // once, so we don't overwrite the ready state that loadItem() just set (find() waits for idle).
216
227
  const service = item.getService();
217
228
  const subscription = service.subscribe((snapshot) => {
218
229
  // Update loading state based on service state changes
219
230
  if (snapshot && typeof snapshot === 'object' && 'value' in snapshot) {
220
231
  const isIdle = snapshot.value === 'idle';
221
- setIsLoading(!isIdle);
222
- // Clear error if service is in idle state
223
232
  if (isIdle) {
233
+ hasSeenIdleRef.current = true;
234
+ setIsLoading(false);
224
235
  setError(null);
225
236
  }
226
237
  else if (snapshot.value === 'error') {
227
- // Set error if service is in error state
228
238
  setError(new Error('Item service error'));
239
+ setIsLoading(false);
240
+ }
241
+ else {
242
+ // Only show loading if we've already seen idle (real transition to loading)
243
+ if (hasSeenIdleRef.current) {
244
+ setIsLoading(true);
245
+ }
229
246
  }
230
247
  }
231
248
  });
@@ -241,19 +258,21 @@ const useItem = ({ modelName, seedLocalId, seedUid }) => {
241
258
  error,
242
259
  };
243
260
  };
261
+ const getItemsQueryKey = (modelName, deleted) => ['seed', 'items', modelName ?? null, deleted ?? false];
244
262
  const useItems = ({ modelName, deleted = false }) => {
245
- const [items, setItems] = useState([]);
246
- const [isLoading, setIsLoading] = useState(false);
247
- const [error, setError] = useState(null);
248
263
  const isClientReady = useIsClientReady();
249
- const subscriptionsRef = useRef(new Map());
250
- const loadingItemsRef = useRef(new Set());
264
+ const queryClient = useQueryClient();
251
265
  const previousSeedsTableDataRef = useRef(undefined);
252
- const itemsRef = useRef([]); // Track items for comparison without triggering effects
266
+ const itemsRef = useRef([]);
267
+ const lastFetchedIdsRef = useRef(new Set());
268
+ const queryKey = useMemo(() => getItemsQueryKey(modelName, deleted), [modelName, deleted]);
269
+ const { data: items = [], isLoading, error: queryError, } = useQuery({
270
+ queryKey,
271
+ queryFn: () => Item.all(modelName, deleted, { waitForReady: true }),
272
+ enabled: isClientReady,
273
+ });
274
+ itemsRef.current = items;
253
275
  // Watch the seeds table for changes
254
- // Memoize the query so it's stable across renders - this is critical for distinctUntilChanged to work
255
- // IMPORTANT: This query must match the logic in getItemsData() to ensure seedsTableData
256
- // only includes seeds that Item.all() will return (i.e., seeds with versionsCount > 0)
257
276
  const db = isClientReady ? BaseDb.getAppDb() : null;
258
277
  const seedsQuery = useMemo(() => {
259
278
  if (!db)
@@ -268,8 +287,6 @@ const useItems = ({ modelName, deleted = false }) => {
268
287
  else {
269
288
  conditions.push(or(isNull(seeds._markedForDeletion), eq(seeds._markedForDeletion, 0)));
270
289
  }
271
- // Join with versionData and filter by versionsCount > 0 to match getItemsData() logic
272
- // This ensures we only watch seeds that have at least one version
273
290
  const versionData = getVersionData();
274
291
  return db
275
292
  .with(versionData)
@@ -288,162 +305,39 @@ const useItems = ({ modelName, deleted = false }) => {
288
305
  .groupBy(seeds.localId);
289
306
  }, [db, isClientReady, modelName, deleted]);
290
307
  const seedsTableData = useLiveQuery(seedsQuery);
291
- const fetchItems = useCallback(async () => {
292
- try {
293
- setIsLoading(true);
294
- setError(null);
295
- const allItems = await Item.all(modelName, deleted);
296
- // Filter items into ready vs loading based on service state
297
- const readyItems = [];
298
- const loadingItemsList = [];
299
- // Clear previous loading set
300
- loadingItemsRef.current.clear();
301
- for (const item of allItems) {
302
- const snapshot = item.getService().getSnapshot();
303
- const isIdle = snapshot.value === 'idle';
304
- if (isIdle) {
305
- // Item is ready
306
- readyItems.push(item);
307
- }
308
- else {
309
- // Item is still loading - subscribe to state changes
310
- loadingItemsList.push(item);
311
- loadingItemsRef.current.add(item);
312
- // Clean up any existing subscription for this item
313
- const existingSub = subscriptionsRef.current.get(item);
314
- if (existingSub) {
315
- existingSub.unsubscribe();
316
- }
317
- // Subscribe to state changes
318
- const subscription = item.getService().subscribe((snapshot) => {
319
- if (snapshot && typeof snapshot === 'object' && 'value' in snapshot) {
320
- const isIdle = snapshot.value === 'idle';
321
- if (isIdle) {
322
- // Item is now ready - update state
323
- setItems(prev => {
324
- // Check if item is already in the list (by seedLocalId or seedUid)
325
- const exists = prev.some(i => (i.seedLocalId && item.seedLocalId && i.seedLocalId === item.seedLocalId) ||
326
- (i.seedUid && item.seedUid && i.seedUid === item.seedUid));
327
- if (exists) {
328
- return prev;
329
- }
330
- // Add the newly ready item
331
- const updated = [...prev, item];
332
- itemsRef.current = updated; // Update ref for comparison
333
- return updated;
334
- });
335
- // Remove from loading set and clean up subscription
336
- loadingItemsRef.current.delete(item);
337
- subscription.unsubscribe();
338
- subscriptionsRef.current.delete(item);
339
- // Update loading state based on remaining loading items
340
- setIsLoading(loadingItemsRef.current.size > 0);
341
- }
342
- else if (snapshot.value === 'error') {
343
- // Item failed to load - clean up subscription
344
- loadingItemsRef.current.delete(item);
345
- subscription.unsubscribe();
346
- subscriptionsRef.current.delete(item);
347
- // Update loading state based on remaining loading items
348
- setIsLoading(loadingItemsRef.current.size > 0);
349
- }
350
- }
351
- });
352
- subscriptionsRef.current.set(item, subscription);
353
- }
354
- }
355
- // Set initial ready items
356
- setItems(readyItems);
357
- itemsRef.current = readyItems; // Update ref for comparison
358
- setError(null);
359
- setIsLoading(loadingItemsList.length > 0); // Still loading if any items are loading
360
- }
361
- catch (error) {
362
- setError(error);
363
- setIsLoading(false);
364
- }
365
- }, [modelName, deleted]);
366
- // Cleanup subscriptions for items that are no longer in the list
367
- useEffect(() => {
368
- const currentItemKeys = new Set();
369
- for (const item of items) {
370
- const key = item.seedLocalId || item.seedUid || '';
371
- if (key) {
372
- currentItemKeys.add(key);
373
- }
374
- }
375
- // Clean up subscriptions for items that are no longer in the list
376
- for (const [item, subscription] of subscriptionsRef.current.entries()) {
377
- const key = item.seedLocalId || item.seedUid || '';
378
- if (key && !currentItemKeys.has(key)) {
379
- // Item is no longer in the list, clean up subscription
380
- subscription.unsubscribe();
381
- subscriptionsRef.current.delete(item);
382
- loadingItemsRef.current.delete(item);
383
- }
384
- }
385
- // Update loading state based on remaining loading items
386
- if (loadingItemsRef.current.size === 0 && isLoading) {
387
- setIsLoading(false);
388
- }
389
- }, [items, isLoading]);
390
- // Fetch items on initial mount when client is ready
391
- useEffect(() => {
392
- if (!isClientReady) {
393
- return;
394
- }
395
- // Initial fetch when client becomes ready
396
- fetchItems();
397
- }, [isClientReady, fetchItems]);
398
- // Refetch items when table data actually changes (not just reference)
308
+ // Invalidate when table data actually changes so useQuery refetches
399
309
  useEffect(() => {
400
- if (!isClientReady || !seedsTableData) {
401
- return;
402
- }
403
- // Check if seedsTableData actually changed by comparing with previous value
404
- const prevData = previousSeedsTableDataRef.current;
405
- const prevDataJson = prevData ? JSON.stringify(prevData.map(s => ({ localId: s.localId, uid: s.uid }))) : 'undefined';
406
- const currDataJson = seedsTableData ? JSON.stringify(seedsTableData.map(s => ({ localId: s.localId, uid: s.uid }))) : 'undefined';
407
- if (prevDataJson === currDataJson && prevData !== undefined) {
408
- // Data hasn't actually changed, skip refetch
310
+ if (!isClientReady || !seedsTableData)
409
311
  return;
312
+ const tableDataItemsSet = new Set();
313
+ for (const dbSeed of seedsTableData) {
314
+ const key = dbSeed.localId || dbSeed.uid;
315
+ if (key)
316
+ tableDataItemsSet.add(key);
410
317
  }
411
- // Update ref with current data
412
- previousSeedsTableDataRef.current = seedsTableData;
413
- // Extract identifying information from current items in state (using ref to avoid dependency)
414
318
  const currentItemsSet = new Set();
415
319
  for (const item of itemsRef.current) {
416
320
  const key = item.seedLocalId || item.seedUid;
417
- if (key) {
321
+ if (key)
418
322
  currentItemsSet.add(key);
419
- }
420
323
  }
421
- // Extract identifying information from seedsTableData
422
- const tableDataItemsSet = new Set();
423
- for (const dbSeed of seedsTableData) {
424
- const key = dbSeed.localId || dbSeed.uid;
425
- if (key) {
426
- tableDataItemsSet.add(key);
427
- }
324
+ if (tableDataItemsSet.size === 0 && currentItemsSet.size > 0)
325
+ return;
326
+ const lastFetched = lastFetchedIdsRef.current;
327
+ if (lastFetched.size === tableDataItemsSet.size &&
328
+ [...lastFetched].every((id) => tableDataItemsSet.has(id))) {
329
+ return;
428
330
  }
429
- // Compare sets to detect changes
331
+ previousSeedsTableDataRef.current = seedsTableData;
430
332
  const setsAreEqual = currentItemsSet.size === tableDataItemsSet.size &&
431
- [...currentItemsSet].every(id => tableDataItemsSet.has(id));
333
+ [...currentItemsSet].every((id) => tableDataItemsSet.has(id));
432
334
  if (setsAreEqual) {
433
- // Items in state match table data, skip refetch
335
+ lastFetchedIdsRef.current = new Set(tableDataItemsSet);
434
336
  return;
435
337
  }
436
- // Items have changed, fetch updated items
437
- fetchItems();
438
- }, [isClientReady, seedsTableData, fetchItems, modelName]);
439
- // Cleanup all subscriptions on unmount
440
- useEffect(() => {
441
- return () => {
442
- subscriptionsRef.current.forEach(sub => sub.unsubscribe());
443
- subscriptionsRef.current.clear();
444
- loadingItemsRef.current.clear();
445
- };
446
- }, []);
338
+ lastFetchedIdsRef.current = new Set(tableDataItemsSet);
339
+ queryClient.invalidateQueries({ queryKey });
340
+ }, [isClientReady, seedsTableData, queryClient, queryKey]);
447
341
  return {
448
342
  items: orderBy(items, [
449
343
  (item) => item.lastVersionPublishedAt ||
@@ -451,28 +345,92 @@ const useItems = ({ modelName, deleted = false }) => {
451
345
  item.createdAt,
452
346
  ], ['desc']),
453
347
  isLoading,
454
- error,
348
+ error: queryError,
455
349
  };
456
350
  };
457
351
  const useCreateItem = () => {
458
- const [isCreatingItem, setIsCreatingItem] = useState(false);
352
+ const [isLoading, setIsLoading] = useState(false);
353
+ const [error, setError] = useState(null);
354
+ const resetError = useCallback(() => setError(null), []);
459
355
  const createItem = useCallback(async (modelName, itemData) => {
460
- if (isCreatingItem) {
461
- // TODO: should we setup a queue for this?
462
- console.error(`[useCreateItem] [createItem] already creating item`, itemData);
463
- return;
356
+ if (isLoading) {
357
+ logger$2('[useCreateItem] [createItem] already creating item, skipping');
358
+ return undefined;
359
+ }
360
+ setError(null);
361
+ // Flush loading=true synchronously so the UI (and tests) can observe it before async work runs.
362
+ flushSync(() => setIsLoading(true));
363
+ try {
364
+ const data = itemData ?? {};
365
+ const { seedLocalId } = await createNewItem({ modelName, ...data });
366
+ const newItem = await Item.find({ modelName, seedLocalId });
367
+ return (newItem ?? undefined);
368
+ }
369
+ catch (err) {
370
+ logger$2('[useCreateItem] Error creating item:', err);
371
+ setError(err instanceof Error ? err : new Error(String(err)));
372
+ return undefined;
464
373
  }
465
- setIsCreatingItem(true);
466
- if (!itemData) {
467
- itemData = {};
374
+ finally {
375
+ // Defer clearing loading so React can commit the loading=true render first.
376
+ // Otherwise the test (or UI) may never observe isLoading true (same continuation batching).
377
+ queueMicrotask(() => setIsLoading(false));
468
378
  }
469
- const { seedLocalId } = await createNewItem({ modelName, ...itemData });
470
- await Item.find({ modelName, seedLocalId });
471
- setIsCreatingItem(false);
472
- }, [isCreatingItem]);
379
+ }, [isLoading]);
473
380
  return {
474
381
  createItem,
475
- isCreatingItem,
382
+ isLoading,
383
+ error,
384
+ resetError,
385
+ };
386
+ };
387
+ const usePublishItem = () => {
388
+ const [publishingItem, setPublishingItem] = useState(null);
389
+ const [isLoading, setIsLoading] = useState(false);
390
+ const [error, setError] = useState(null);
391
+ const subscriptionRef = useRef(undefined);
392
+ const resetError = useCallback(() => setError(null), []);
393
+ const publishItem = useCallback((item) => {
394
+ if (!item)
395
+ return;
396
+ setPublishingItem(item);
397
+ setError(null);
398
+ item.publish().catch(() => {
399
+ // Error is surfaced via service state subscription; avoid unhandled rejection
400
+ });
401
+ }, []);
402
+ useEffect(() => {
403
+ if (!publishingItem) {
404
+ subscriptionRef.current?.unsubscribe();
405
+ subscriptionRef.current = undefined;
406
+ setIsLoading(false);
407
+ return;
408
+ }
409
+ subscriptionRef.current?.unsubscribe();
410
+ const service = publishingItem.getService();
411
+ const subscription = service.subscribe((snapshot) => {
412
+ const value = snapshot?.value;
413
+ const ctx = snapshot?.context;
414
+ setIsLoading(value === 'publishing');
415
+ const publishError = ctx?._publishError;
416
+ setError(publishError ? new Error(publishError.message) : null);
417
+ });
418
+ subscriptionRef.current = subscription;
419
+ const snap = service.getSnapshot();
420
+ setIsLoading(snap?.value === 'publishing');
421
+ const ctx = snap?.context;
422
+ const publishError = ctx?._publishError;
423
+ setError(publishError ? new Error(publishError.message) : null);
424
+ return () => {
425
+ subscriptionRef.current?.unsubscribe();
426
+ subscriptionRef.current = undefined;
427
+ };
428
+ }, [publishingItem]);
429
+ return {
430
+ publishItem,
431
+ isLoading,
432
+ error,
433
+ resetError,
476
434
  };
477
435
  };
478
436
 
@@ -485,36 +443,38 @@ function useItemProperty(arg1, arg2) {
485
443
  const [error, setError] = useState(null);
486
444
  const subscriptionRef = useRef(undefined);
487
445
  const [, setVersion] = useState(0); // Version counter to force re-renders
488
- // Determine which lookup mode we're in based on arguments
446
+ // Extract primitives so useMemo/useCallback deps are stable when caller passes inline objects
447
+ // Support object form with itemId: useItemProperty({ itemId, propertyName })
448
+ const arg1IsObject = typeof arg1 === 'object' && arg1 != null;
449
+ const obj = arg1IsObject ? arg1 : null;
450
+ const itemIdFromObj = obj != null ? obj.itemId : undefined;
451
+ const seedLocalId = obj != null ? obj.seedLocalId : undefined;
452
+ const seedUid = obj != null ? obj.seedUid : undefined;
453
+ const propertyNameFromObj = obj != null ? obj.propertyName : undefined;
454
+ const itemId = typeof arg1 === 'string' ? arg1 : (itemIdFromObj !== undefined && itemIdFromObj !== '' ? itemIdFromObj : undefined);
455
+ const propertyNameFromArgs = typeof arg1 === 'string' ? arg2 : undefined;
456
+ const propertyName = propertyNameFromObj ?? propertyNameFromArgs;
457
+ // Determine which lookup mode we're in based on arguments (deps are primitives to avoid infinite loop)
458
+ // Unify itemId and identifiers: when itemId is provided (string or object form), use it as seedLocalId so we hit the same code path
489
459
  const lookupMode = useMemo(() => {
490
- if (typeof arg1 === 'string' && arg2 !== undefined) {
491
- // Two arguments: itemId, propertyName
492
- return { type: 'itemId', itemId: arg1, propertyName: arg2 };
493
- }
494
- else if (typeof arg1 === 'object') {
495
- // Object argument: { seedLocalId/seedUid, propertyName }
460
+ const resolvedSeedLocalId = (itemId !== undefined && itemId !== '') ? itemId : seedLocalId;
461
+ const resolvedSeedUid = (itemId !== undefined && itemId !== '') ? undefined : seedUid;
462
+ if ((resolvedSeedLocalId != null || resolvedSeedUid != null) && propertyName != null && propertyName !== '') {
496
463
  return {
497
464
  type: 'identifiers',
498
- seedLocalId: arg1.seedLocalId,
499
- seedUid: arg1.seedUid,
500
- propertyName: arg1.propertyName,
465
+ seedLocalId: resolvedSeedLocalId ?? undefined,
466
+ seedUid: resolvedSeedUid,
467
+ propertyName,
501
468
  };
502
469
  }
503
- else {
504
- return null;
505
- }
506
- }, [arg1, arg2]);
470
+ return null;
471
+ }, [itemId, propertyName, seedLocalId, seedUid]);
507
472
  // Determine initial loading state
508
473
  useMemo(() => {
509
474
  if (!lookupMode)
510
475
  return false;
511
- if (lookupMode.type === 'itemId') {
512
- return !!(lookupMode.itemId && lookupMode.propertyName);
513
- }
514
- else {
515
- return !!((lookupMode.seedLocalId || lookupMode.seedUid) &&
516
- lookupMode.propertyName);
517
- }
476
+ return !!((lookupMode.seedLocalId || lookupMode.seedUid) &&
477
+ lookupMode.propertyName);
518
478
  }, [lookupMode]);
519
479
  // Determine if we should be loading based on parameters
520
480
  const shouldLoad = useMemo(() => {
@@ -522,13 +482,8 @@ function useItemProperty(arg1, arg2) {
522
482
  return false;
523
483
  if (!lookupMode)
524
484
  return false;
525
- if (lookupMode.type === 'itemId') {
526
- return !!(lookupMode.itemId && lookupMode.propertyName);
527
- }
528
- else {
529
- return !!((lookupMode.seedLocalId || lookupMode.seedUid) &&
530
- lookupMode.propertyName);
531
- }
485
+ return !!((lookupMode.seedLocalId || lookupMode.seedUid) &&
486
+ lookupMode.propertyName);
532
487
  }, [isClientReady, lookupMode]);
533
488
  const updateItemProperty = useCallback(async () => {
534
489
  if (!isClientReady || !lookupMode) {
@@ -540,17 +495,8 @@ function useItemProperty(arg1, arg2) {
540
495
  try {
541
496
  setIsLoading(true);
542
497
  setError(null);
543
- let seedLocalId;
544
- let seedUid;
545
- if (lookupMode.type === 'itemId') {
546
- // Resolve itemId to seedLocalId/seedUid
547
- // For now, assume itemId is seedLocalId (could be enhanced to support seedUid)
548
- seedLocalId = lookupMode.itemId;
549
- }
550
- else {
551
- seedLocalId = lookupMode.seedLocalId;
552
- seedUid = lookupMode.seedUid;
553
- }
498
+ const seedLocalId = lookupMode.seedLocalId;
499
+ const seedUid = lookupMode.seedUid;
554
500
  if (!seedLocalId && !seedUid) {
555
501
  setProperty(undefined);
556
502
  setIsLoading(false);
@@ -581,7 +527,12 @@ function useItemProperty(arg1, arg2) {
581
527
  setError(error);
582
528
  }
583
529
  }, [isClientReady, lookupMode]);
584
- // Fetch/refetch when lookup parameters change or client becomes ready
530
+ // Fetch/refetch when lookup parameters change or client becomes ready.
531
+ // Skip refetch when we already have the property for this lookup (avoids setting loading true
532
+ // again when effect re-runs e.g. from Strict Mode or updateItemProperty identity change).
533
+ // Match by the active identifier only: when looking up by seedLocalId both must match;
534
+ // when looking up by seedUid both must match. Do not use (seedUid === undefined) as a match
535
+ // when seedLocalIds differ, which would incorrectly skip refetch after seedLocalId change.
585
536
  useEffect(() => {
586
537
  if (!shouldLoad) {
587
538
  setProperty(undefined);
@@ -589,8 +540,15 @@ function useItemProperty(arg1, arg2) {
589
540
  setError(null);
590
541
  return;
591
542
  }
543
+ const alreadyHavePropertyGuard = property &&
544
+ lookupMode &&
545
+ property.propertyName === lookupMode.propertyName &&
546
+ ((lookupMode.seedLocalId != null && property.seedLocalId === lookupMode.seedLocalId) ||
547
+ (lookupMode.seedUid != null && property.seedUid === lookupMode.seedUid));
548
+ if (alreadyHavePropertyGuard)
549
+ return;
592
550
  updateItemProperty();
593
- }, [shouldLoad, updateItemProperty]);
551
+ }, [shouldLoad, updateItemProperty, property, lookupMode]);
594
552
  // Subscribe to service changes when property is available
595
553
  useEffect(() => {
596
554
  if (!property) {
@@ -601,17 +559,13 @@ function useItemProperty(arg1, arg2) {
601
559
  }
602
560
  // Clean up previous subscription
603
561
  subscriptionRef.current?.unsubscribe();
604
- // Subscribe to service changes
562
+ // Subscribe to service changes. Only set isLoading to false when idle; never set to true
563
+ // here so we never overwrite the loaded state when the machine emits any non-idle state
564
+ // (e.g. loading, initializing, resolvingRelatedValue) after the initial fetch.
605
565
  const subscription = property.getService().subscribe((snapshot) => {
606
- // Update loading state based on service state
607
- // Use type guard to check if snapshot has 'value' property
608
- if (snapshot && typeof snapshot === 'object' && 'value' in snapshot) {
609
- const isIdle = snapshot.value === 'idle';
610
- setIsLoading(!isIdle);
611
- // Clear error if service is in idle state
612
- if (isIdle) {
613
- setError(null);
614
- }
566
+ if (snapshot && typeof snapshot === 'object' && 'value' in snapshot && snapshot.value === 'idle') {
567
+ setIsLoading(false);
568
+ setError(null);
615
569
  }
616
570
  // Force re-render by incrementing version counter
617
571
  setVersion(prev => prev + 1);
@@ -628,13 +582,109 @@ function useItemProperty(arg1, arg2) {
628
582
  error,
629
583
  };
630
584
  }
585
+ /** Fetches item properties list for useQuery (shared with useItemProperties). */
586
+ async function fetchItemPropertiesList(seedLocalId, seedUid) {
587
+ if (!seedLocalId && !seedUid)
588
+ return [];
589
+ const db = BaseDb.getAppDb();
590
+ if (!db)
591
+ return [];
592
+ const baseList = await ItemProperty.all({ seedLocalId: seedLocalId ?? undefined, seedUid: seedUid ?? undefined }, { waitForReady: true });
593
+ const _itemProperties = [...baseList];
594
+ const propertiesWithMetadata = new Set();
595
+ for (const p of baseList) {
596
+ if (p.propertyName)
597
+ propertiesWithMetadata.add(p.propertyName);
598
+ }
599
+ let modelName;
600
+ if (baseList.length > 0) {
601
+ const first = baseList[0];
602
+ modelName = first.modelName ?? first.modelType;
603
+ if (modelName && typeof modelName === 'string')
604
+ modelName = startCase(modelName);
605
+ }
606
+ if (!modelName) {
607
+ const seedRecords = await db
608
+ .select({ type: seeds.type })
609
+ .from(seeds)
610
+ .where(seedUid ? eq(seeds.uid, seedUid) : eq(seeds.localId, seedLocalId))
611
+ .limit(1);
612
+ if (seedRecords.length > 0 && seedRecords[0].type) {
613
+ modelName = startCase(seedRecords[0].type);
614
+ }
615
+ }
616
+ const modelProperties = [];
617
+ if (modelName) {
618
+ try {
619
+ const { Model } = await import('./index-DPll6EAp.js').then(function (n) { return n.aW; });
620
+ const model = await Model.getByNameAsync(modelName);
621
+ if (model?.properties) {
622
+ for (const modelProperty of model.properties) {
623
+ if (modelProperty.name)
624
+ modelProperties.push(modelProperty.name);
625
+ }
626
+ }
627
+ }
628
+ catch (error) {
629
+ propertiesLogger(`[useItemProperties] Error getting ModelProperties for ${modelName}:`, error);
630
+ }
631
+ }
632
+ if (modelName && modelProperties.length > 0) {
633
+ const resolvedSeedLocalId = baseList.length > 0 ? (baseList[0].seedLocalId ?? seedLocalId) : seedLocalId;
634
+ const resolvedSeedUid = baseList.length > 0 ? (baseList[0].seedUid ?? seedUid) : seedUid;
635
+ for (const propertyName of modelProperties) {
636
+ if (propertiesWithMetadata.has(propertyName))
637
+ continue;
638
+ try {
639
+ const itemProperty = ItemProperty.create({
640
+ propertyName,
641
+ modelName,
642
+ seedLocalId: resolvedSeedLocalId || undefined,
643
+ seedUid: resolvedSeedUid || undefined,
644
+ propertyValue: null,
645
+ }, { waitForReady: false });
646
+ if (itemProperty)
647
+ _itemProperties.push(itemProperty);
648
+ }
649
+ catch (error) {
650
+ logger$1(`[useItemProperties] Error creating ItemProperty for missing property ${propertyName}:`, error);
651
+ }
652
+ }
653
+ }
654
+ if (seedLocalId || seedUid) {
655
+ const seedRecords = await db
656
+ .select({ createdAt: seeds.createdAt })
657
+ .from(seeds)
658
+ .where(seedUid ? eq(seeds.uid, seedUid) : eq(seeds.localId, seedLocalId))
659
+ .limit(1);
660
+ if (seedRecords.length > 0 && seedRecords[0].createdAt) {
661
+ const createdAtPropertyName = 'createdAt';
662
+ const hasCreatedAtProperty = _itemProperties.some((p) => p.propertyName === createdAtPropertyName);
663
+ if (!hasCreatedAtProperty && modelName) {
664
+ try {
665
+ const resolvedSeedLocalId = baseList.length > 0 ? (baseList[0].seedLocalId ?? seedLocalId) : seedLocalId;
666
+ const resolvedSeedUid = baseList.length > 0 ? (baseList[0].seedUid ?? seedUid) : seedUid;
667
+ const createdAtProperty = ItemProperty.create({
668
+ propertyName: createdAtPropertyName,
669
+ modelName,
670
+ seedLocalId: resolvedSeedLocalId || undefined,
671
+ seedUid: resolvedSeedUid || undefined,
672
+ propertyValue: seedRecords[0].createdAt.toString(),
673
+ }, { waitForReady: false });
674
+ if (createdAtProperty)
675
+ _itemProperties.push(createdAtProperty);
676
+ }
677
+ catch (error) {
678
+ logger$1(`[useItemProperties] Error creating createdAt ItemProperty:`, error);
679
+ }
680
+ }
681
+ }
682
+ }
683
+ return _itemProperties;
684
+ }
631
685
  function useItemProperties(arg1) {
632
- const [properties, setProperties] = useState([]);
633
- const [isLoading, setIsLoading] = useState(false);
634
- const [error, setError] = useState(null);
635
686
  const isClientReady = useIsClientReady();
636
- const subscriptionsRef = useRef(new Map());
637
- const loadingPropertiesRef = useRef(new Set());
687
+ const queryClient = useQueryClient();
638
688
  const previousTableDataRef = useRef(undefined);
639
689
  // Determine which lookup mode we're in based on arguments
640
690
  const lookupMode = useMemo(() => {
@@ -670,6 +720,13 @@ function useItemProperties(arg1) {
670
720
  return undefined;
671
721
  return lookupMode.seedUid;
672
722
  }, [lookupMode]);
723
+ const canonicalItemKey = seedLocalId ?? seedUid ?? '';
724
+ const itemPropertiesQueryKey = useMemo(() => ['seed', 'itemProperties', canonicalItemKey], [canonicalItemKey]);
725
+ const { data: properties = [], isLoading, error: queryError, } = useQuery({
726
+ queryKey: itemPropertiesQueryKey,
727
+ queryFn: () => fetchItemPropertiesList(seedLocalId, seedUid),
728
+ enabled: isClientReady && !!canonicalItemKey,
729
+ });
673
730
  // Watch the metadata table for changes
674
731
  // Query metadata table directly and filter for latest records in JavaScript
675
732
  // This is simpler and works better with useLiveQuery than CTEs
@@ -721,20 +778,6 @@ function useItemProperties(arg1) {
721
778
  return query;
722
779
  }, [isClientReady, seedLocalId, seedUid]);
723
780
  const rawPropertiesTableData = useLiveQuery(propertiesQuery);
724
- // Debug logging for rawPropertiesTableData
725
- useEffect(() => {
726
- if (rawPropertiesTableData !== undefined) {
727
- propertiesLogger(`[useItemProperties] rawPropertiesTableData updated:`, {
728
- length: rawPropertiesTableData?.length || 0,
729
- isUndefined: rawPropertiesTableData === undefined,
730
- isArray: Array.isArray(rawPropertiesTableData),
731
- firstRecord: rawPropertiesTableData?.[0] || null,
732
- });
733
- }
734
- else {
735
- propertiesLogger('[useItemProperties] rawPropertiesTableData is undefined (query not executed yet)');
736
- }
737
- }, [rawPropertiesTableData]);
738
781
  // Filter for latest records (one per propertyName) in JavaScript
739
782
  const propertiesTableData = useMemo(() => {
740
783
  if (!rawPropertiesTableData || rawPropertiesTableData.length === 0) {
@@ -760,358 +803,131 @@ function useItemProperties(arg1) {
760
803
  }
761
804
  return Array.from(latestByProperty.values());
762
805
  }, [rawPropertiesTableData]);
763
- const fetchItemProperties = useCallback(async () => {
764
- if (!seedLocalId && !seedUid) {
765
- propertiesLogger('[useItemProperties] fetchItemProperties: no identifiers, clearing properties');
766
- setProperties([]);
767
- setIsLoading(false);
768
- setError(null);
806
+ // Invalidate when metadata table data actually changes so useQuery refetches
807
+ useEffect(() => {
808
+ if (!isClientReady || (!seedLocalId && !seedUid) || propertiesTableData === undefined)
769
809
  return;
770
- }
771
- // Don't fetch if propertiesTableData is not available yet
772
- if (propertiesTableData === undefined) {
773
- propertiesLogger('[useItemProperties] fetchItemProperties: propertiesTableData is undefined, skipping');
810
+ // Include propertyValue so value-only changes produce a different string and trigger invalidation
811
+ const tableDataString = JSON.stringify(propertiesTableData
812
+ .map((p) => ({
813
+ propertyName: p.propertyName,
814
+ propertyValue: p.propertyValue,
815
+ seedLocalId: p.seedLocalId,
816
+ seedUid: p.seedUid,
817
+ }))
818
+ .sort((a, b) => (a.propertyName || '').localeCompare(b.propertyName || '')));
819
+ if (previousTableDataRef.current === tableDataString)
774
820
  return;
821
+ previousTableDataRef.current = tableDataString;
822
+ // Invalidate when metadata table data changed (new/updated/removed props or value changes)
823
+ // so useQuery refetches and UI shows latest values.
824
+ if (propertiesTableData.length > 0) {
825
+ queryClient.invalidateQueries({ queryKey: itemPropertiesQueryKey });
775
826
  }
776
- propertiesLogger(`[useItemProperties] fetchItemProperties: starting with ${propertiesTableData?.length || 0} records from table`);
777
- try {
778
- setIsLoading(true);
779
- setError(null);
780
- const db = BaseDb.getAppDb();
781
- if (!db) {
782
- propertiesLogger('[useItemProperties] fetchItemProperties: no db available');
783
- setProperties([]);
784
- setIsLoading(false);
785
- return;
786
- }
787
- // Get modelName from metadata records or from seeds table
788
- let modelName;
789
- if (propertiesTableData && propertiesTableData.length > 0) {
790
- const firstProperty = propertiesTableData[0];
791
- if (firstProperty.modelType) {
792
- modelName = startCase(firstProperty.modelType);
793
- }
794
- }
795
- // If we don't have modelName from metadata, try to get it from seeds table
796
- if (!modelName) {
797
- const seedRecords = await db
798
- .select({ type: seeds.type })
799
- .from(seeds)
800
- .where(seedUid ? eq(seeds.uid, seedUid) : eq(seeds.localId, seedLocalId))
801
- .limit(1);
802
- if (seedRecords.length > 0 && seedRecords[0].type) {
803
- modelName = startCase(seedRecords[0].type);
804
- }
805
- }
806
- // Get all ModelProperties for this Model
807
- const modelProperties = [];
808
- if (modelName) {
809
- try {
810
- const { Model } = await import('./json-I3vJhXo8.js').then(function (n) { return n.aS; });
811
- const model = await Model.getByNameAsync(modelName);
812
- if (model && model.properties) {
813
- for (const modelProperty of model.properties) {
814
- if (modelProperty.name) {
815
- modelProperties.push(modelProperty.name);
816
- }
817
- }
818
- }
819
- }
820
- catch (error) {
821
- propertiesLogger(`[useItemProperties] Error getting ModelProperties for ${modelName}:`, error);
822
- // Continue without ModelProperties - we'll still return properties from metadata
823
- }
824
- }
825
- // Create a Set of property names that have metadata records
826
- const propertiesWithMetadata = new Set();
827
- if (propertiesTableData) {
828
- for (const dbProperty of propertiesTableData) {
829
- if (dbProperty.propertyName) {
830
- propertiesWithMetadata.add(dbProperty.propertyName);
831
- }
832
- }
833
- }
834
- const _itemProperties = [];
835
- // First, create ItemProperty instances for properties that have metadata records
836
- if (propertiesTableData && propertiesTableData.length > 0) {
837
- for (const dbProperty of propertiesTableData) {
838
- if (!dbProperty.propertyName) {
839
- continue;
840
- }
841
- try {
842
- const itemProperty = await ItemProperty.find({
843
- propertyName: dbProperty.propertyName,
844
- seedLocalId: dbProperty.seedLocalId || undefined,
845
- seedUid: dbProperty.seedUid || undefined,
846
- });
847
- if (itemProperty) {
848
- _itemProperties.push(itemProperty);
849
- }
850
- }
851
- catch (error) {
852
- logger$1(`[useItemProperties] Error creating ItemProperty for ${dbProperty.propertyName}:`, error);
853
- // Continue with other properties even if one fails
854
- }
855
- }
856
- }
857
- // Then, create ItemProperty instances for ModelProperties that don't have metadata records
858
- if (modelName && modelProperties.length > 0) {
859
- const resolvedSeedLocalId = propertiesTableData && propertiesTableData.length > 0
860
- ? propertiesTableData[0].seedLocalId || seedLocalId
861
- : seedLocalId;
862
- const resolvedSeedUid = propertiesTableData && propertiesTableData.length > 0
863
- ? propertiesTableData[0].seedUid || seedUid
864
- : seedUid;
865
- for (const propertyName of modelProperties) {
866
- // Skip if we already have a metadata record for this property
867
- if (propertiesWithMetadata.has(propertyName)) {
868
- continue;
869
- }
870
- try {
871
- // Create ItemProperty with empty value for properties without metadata records
872
- const itemProperty = ItemProperty.create({
873
- propertyName,
874
- modelName,
875
- seedLocalId: resolvedSeedLocalId || undefined,
876
- seedUid: resolvedSeedUid || undefined,
877
- propertyValue: null,
878
- });
879
- if (itemProperty) {
880
- _itemProperties.push(itemProperty);
881
- }
882
- }
883
- catch (error) {
884
- logger$1(`[useItemProperties] Error creating ItemProperty for missing property ${propertyName}:`, error);
885
- // Continue with other properties even if one fails
886
- }
887
- }
888
- }
889
- // Also add system properties like 'createdAt' if they don't exist
890
- // Get createdAt from seeds table
891
- if (seedLocalId || seedUid) {
892
- const seedRecords = await db
893
- .select({ createdAt: seeds.createdAt })
894
- .from(seeds)
895
- .where(seedUid ? eq(seeds.uid, seedUid) : eq(seeds.localId, seedLocalId))
896
- .limit(1);
897
- if (seedRecords.length > 0 && seedRecords[0].createdAt) {
898
- const createdAtPropertyName = 'createdAt';
899
- const hasCreatedAtProperty = _itemProperties.some(p => p.propertyName === createdAtPropertyName);
900
- if (!hasCreatedAtProperty && modelName) {
901
- try {
902
- const resolvedSeedLocalId = propertiesTableData && propertiesTableData.length > 0
903
- ? propertiesTableData[0].seedLocalId || seedLocalId
904
- : seedLocalId;
905
- const resolvedSeedUid = propertiesTableData && propertiesTableData.length > 0
906
- ? propertiesTableData[0].seedUid || seedUid
907
- : seedUid;
908
- const createdAtProperty = ItemProperty.create({
909
- propertyName: createdAtPropertyName,
910
- modelName,
911
- seedLocalId: resolvedSeedLocalId || undefined,
912
- seedUid: resolvedSeedUid || undefined,
913
- propertyValue: seedRecords[0].createdAt.toString(),
914
- });
915
- if (createdAtProperty) {
916
- _itemProperties.push(createdAtProperty);
917
- }
918
- }
919
- catch (error) {
920
- logger$1(`[useItemProperties] Error creating createdAt ItemProperty:`, error);
921
- }
922
- }
923
- }
924
- }
925
- // Filter out properties that are ready (idle state) vs still loading
926
- const readyProperties = [];
927
- const loadingPropertiesList = [];
928
- // Clear previous loading set
929
- loadingPropertiesRef.current.clear();
930
- for (const property of _itemProperties) {
931
- const snapshot = property.getService().getSnapshot();
932
- // Use type guard to check if snapshot has 'value' property
933
- const isIdle = snapshot && typeof snapshot === 'object' && 'value' in snapshot && snapshot.value === 'idle';
934
- if (isIdle) {
935
- // Property is ready
936
- readyProperties.push(property);
937
- }
938
- else {
939
- // Property is still loading - subscribe to state changes
940
- loadingPropertiesList.push(property);
941
- loadingPropertiesRef.current.add(property);
942
- // Clean up any existing subscription for this property
943
- const existingSub = subscriptionsRef.current.get(property);
944
- if (existingSub) {
945
- existingSub.unsubscribe();
946
- }
947
- // Subscribe to state changes
948
- const subscription = property.getService().subscribe((snapshot) => {
949
- // Use type guard to check if snapshot has 'value' property
950
- if (snapshot && typeof snapshot === 'object' && 'value' in snapshot) {
951
- const isIdle = snapshot.value === 'idle';
952
- if (isIdle) {
953
- // Property is now ready - update state
954
- setProperties(prev => {
955
- // Check if property is already in the list (by propertyName and seedLocalId/seedUid)
956
- const exists = prev.some(p => p.propertyName === property.propertyName &&
957
- (p.seedLocalId === property.seedLocalId || p.seedUid === property.seedUid));
958
- if (exists) {
959
- return prev;
960
- }
961
- // Add the newly ready property
962
- return [...prev, property];
963
- });
964
- // Remove from loading set and clean up subscription
965
- loadingPropertiesRef.current.delete(property);
966
- subscription.unsubscribe();
967
- subscriptionsRef.current.delete(property);
968
- // Update loading state based on remaining loading properties
969
- setIsLoading(loadingPropertiesRef.current.size > 0);
970
- }
971
- else if (snapshot.value === 'error') {
972
- // Property failed to load - clean up subscription
973
- loadingPropertiesRef.current.delete(property);
974
- subscription.unsubscribe();
975
- subscriptionsRef.current.delete(property);
976
- // Update loading state based on remaining loading properties
977
- setIsLoading(loadingPropertiesRef.current.size > 0);
978
- }
979
- }
980
- });
981
- subscriptionsRef.current.set(property, subscription);
982
- }
983
- }
984
- // Set initial ready properties
985
- setProperties(readyProperties);
986
- setError(null);
987
- setIsLoading(loadingPropertiesList.length > 0); // Still loading if any properties are loading
988
- }
989
- catch (error) {
990
- setError(error);
991
- setIsLoading(false);
992
- }
993
- }, [seedLocalId, seedUid, propertiesTableData]);
994
- // Reset previous table data ref when identifiers change
827
+ }, [isClientReady, propertiesTableData, properties, seedLocalId, seedUid, queryClient, itemPropertiesQueryKey]);
995
828
  useEffect(() => {
996
829
  previousTableDataRef.current = undefined;
997
830
  }, [seedLocalId, seedUid]);
998
- // Fetch item properties when dbModelId becomes available
999
- useEffect(() => {
1000
- if (!isClientReady) {
1001
- return;
831
+ return {
832
+ properties,
833
+ isLoading,
834
+ error: queryError,
835
+ };
836
+ }
837
+ /**
838
+ * Hook to create an ItemProperty with loading and error state.
839
+ * create(props) creates a new property instance for an item; provide seedLocalId or seedUid, propertyName, and modelName.
840
+ */
841
+ const useCreateItemProperty = () => {
842
+ const subscriptionRef = useRef(undefined);
843
+ const [isLoading, setIsLoading] = useState(false);
844
+ const [error, setError] = useState(null);
845
+ const resetError = useCallback(() => setError(null), []);
846
+ const create = useCallback((props) => {
847
+ if (!props.propertyName || (!props.seedLocalId && !props.seedUid) || !props.modelName) {
848
+ const err = new Error('seedLocalId or seedUid, propertyName, and modelName are required');
849
+ setError(err);
850
+ return undefined;
1002
851
  }
1003
- if (!seedLocalId && !seedUid) {
1004
- setProperties([]);
852
+ setError(null);
853
+ setIsLoading(true);
854
+ subscriptionRef.current?.unsubscribe();
855
+ subscriptionRef.current = undefined;
856
+ const instance = ItemProperty.create(props, { waitForReady: false });
857
+ if (!instance) {
858
+ setError(new Error('Failed to create item property'));
1005
859
  setIsLoading(false);
1006
- setError(null);
1007
- previousTableDataRef.current = undefined;
1008
- return;
1009
- }
1010
- // Wait for propertiesTableData to be available before initial fetch
1011
- // (it may be undefined initially while the query is starting)
1012
- if (propertiesTableData === undefined) {
1013
- return;
1014
- }
1015
- // Initial fetch when client is ready and propertiesTableData is available
1016
- fetchItemProperties();
1017
- }, [isClientReady, seedLocalId, seedUid, fetchItemProperties, propertiesTableData]);
1018
- // Refetch item properties when table data actually changes (not just reference)
1019
- useEffect(() => {
1020
- if (!isClientReady || (!seedLocalId && !seedUid)) {
1021
- return;
860
+ return undefined;
1022
861
  }
1023
- // If propertiesTableData is undefined, the query hasn't started yet - wait for it
1024
- if (propertiesTableData === undefined) {
1025
- return;
1026
- }
1027
- // Create a stable string representation of the table data for comparison
1028
- const tableDataString = JSON.stringify(propertiesTableData.map(p => ({
1029
- propertyName: p.propertyName,
1030
- seedLocalId: p.seedLocalId,
1031
- seedUid: p.seedUid,
1032
- })).sort((a, b) => (a.propertyName || '').localeCompare(b.propertyName || '')));
1033
- // Skip if table data hasn't actually changed
1034
- if (previousTableDataRef.current === tableDataString) {
1035
- return;
1036
- }
1037
- previousTableDataRef.current = tableDataString;
1038
- // Extract identifying information from current properties in state
1039
- const currentPropertiesSet = new Set();
1040
- for (const prop of properties) {
1041
- const key = `${prop.propertyName}:${prop.seedLocalId || prop.seedUid}`;
1042
- currentPropertiesSet.add(key);
1043
- }
1044
- // Extract identifying information from propertiesTableData
1045
- const tableDataPropertiesSet = new Set();
1046
- for (const dbProperty of propertiesTableData) {
1047
- if (dbProperty.propertyName) {
1048
- const key = `${dbProperty.propertyName}:${dbProperty.seedLocalId || dbProperty.seedUid}`;
1049
- tableDataPropertiesSet.add(key);
862
+ const subscription = instance.getService().subscribe((snapshot) => {
863
+ if (snapshot?.value === 'error') {
864
+ const err = snapshot.context?._loadingError?.error ?? new Error('Failed to create item property');
865
+ setError(err instanceof Error ? err : new Error(String(err)));
866
+ setIsLoading(false);
1050
867
  }
1051
- }
1052
- // Compare sets to detect changes
1053
- // If tableDataPropertiesSet is empty but we have properties, or vice versa, that's a change
1054
- // If both are empty, skip (no data yet) - UNLESS this is the first time we're seeing empty data
1055
- // If both have data and match, skip
1056
- const setsAreEqual = currentPropertiesSet.size === tableDataPropertiesSet.size &&
1057
- currentPropertiesSet.size > 0 &&
1058
- [...currentPropertiesSet].every(id => tableDataPropertiesSet.has(id));
1059
- if (setsAreEqual) {
1060
- // Properties in state match table data, skip refetch
1061
- return;
1062
- }
1063
- // Always refetch if table data has properties (even if we don't have any yet)
1064
- // This handles the case where propertiesTableData changes from empty to having data
1065
- if (tableDataPropertiesSet.size > 0) {
1066
- // Properties have changed or data has arrived, fetch updated properties
1067
- fetchItemProperties();
1068
- return;
1069
- }
1070
- // If table data is empty but we have properties, that's also a change (properties were removed)
1071
- if (currentPropertiesSet.size > 0 && tableDataPropertiesSet.size === 0) {
1072
- fetchItemProperties();
1073
- return;
1074
- }
1075
- // If both are empty, we've already tried to fetch (in the first useEffect)
1076
- // and got empty results, so skip refetching until data arrives
1077
- // (the change detection above will handle when data arrives)
1078
- }, [isClientReady, propertiesTableData, properties, fetchItemProperties, seedLocalId, seedUid]);
1079
- // Cleanup subscriptions for properties that are no longer in the list
1080
- useEffect(() => {
1081
- const currentPropertyKeys = new Set();
1082
- for (const prop of properties) {
1083
- const key = `${prop.propertyName}:${prop.seedLocalId || prop.seedUid}`;
1084
- currentPropertyKeys.add(key);
1085
- }
1086
- // Clean up subscriptions for properties that are no longer in the list
1087
- for (const [property, subscription] of subscriptionsRef.current.entries()) {
1088
- const key = `${property.propertyName}:${property.seedLocalId || property.seedUid}`;
1089
- if (!currentPropertyKeys.has(key)) {
1090
- // Property is no longer in the list, clean up subscription
1091
- subscription.unsubscribe();
1092
- subscriptionsRef.current.delete(property);
1093
- loadingPropertiesRef.current.delete(property);
868
+ if (snapshot?.value === 'idle') {
869
+ setError(null);
870
+ setIsLoading(false);
1094
871
  }
1095
- }
1096
- // Update loading state based on remaining loading properties
1097
- if (loadingPropertiesRef.current.size === 0 && isLoading) {
1098
- setIsLoading(false);
1099
- }
1100
- }, [properties, isLoading]);
1101
- // Cleanup all subscriptions on unmount
872
+ });
873
+ subscriptionRef.current = subscription;
874
+ return instance;
875
+ }, []);
1102
876
  useEffect(() => {
1103
877
  return () => {
1104
- subscriptionsRef.current.forEach(sub => sub.unsubscribe());
1105
- subscriptionsRef.current.clear();
1106
- loadingPropertiesRef.current.clear();
878
+ subscriptionRef.current?.unsubscribe();
879
+ subscriptionRef.current = undefined;
1107
880
  };
1108
881
  }, []);
1109
882
  return {
1110
- properties,
883
+ create,
1111
884
  isLoading,
1112
885
  error,
886
+ resetError,
1113
887
  };
1114
- }
888
+ };
889
+ const useDestroyItemProperty = () => {
890
+ const [currentInstance, setCurrentInstance] = useState(null);
891
+ const [destroyState, setDestroyState] = useState({
892
+ isLoading: false,
893
+ error: null,
894
+ });
895
+ useEffect(() => {
896
+ if (!currentInstance) {
897
+ setDestroyState({ isLoading: false, error: null });
898
+ return;
899
+ }
900
+ const service = currentInstance.getService();
901
+ const update = () => {
902
+ const snap = service.getSnapshot();
903
+ const ctx = snap.context;
904
+ setDestroyState({
905
+ isLoading: !!ctx._destroyInProgress,
906
+ error: ctx._destroyError ? new Error(ctx._destroyError.message) : null,
907
+ });
908
+ };
909
+ update();
910
+ const sub = service.subscribe(update);
911
+ return () => sub.unsubscribe();
912
+ }, [currentInstance]);
913
+ const destroy = useCallback(async (itemProperty) => {
914
+ if (!itemProperty)
915
+ return;
916
+ setCurrentInstance(itemProperty);
917
+ await itemProperty.destroy();
918
+ }, []);
919
+ const resetError = useCallback(() => {
920
+ if (currentInstance) {
921
+ currentInstance.getService().send({ type: 'clearDestroyError' });
922
+ }
923
+ }, [currentInstance]);
924
+ return {
925
+ destroy,
926
+ isLoading: destroyState.isLoading,
927
+ error: destroyState.error,
928
+ resetError,
929
+ };
930
+ };
1115
931
 
1116
932
  debug('seedSdk:react:services');
1117
933
 
@@ -1137,23 +953,31 @@ const useSchema = (schemaIdentifier) => {
1137
953
  setIsLoading(true);
1138
954
  setError(null);
1139
955
  try {
1140
- const schemaInstance = Schema.create(identifier);
956
+ const schemaInstance = Schema.create(identifier, {
957
+ waitForReady: false,
958
+ });
1141
959
  setSchema(schemaInstance);
1142
960
  const service = schemaInstance.getService();
1143
961
  const initialSnapshot = service.getSnapshot();
1144
962
  // Set initial loading state based on whether status is 'idle'
1145
963
  const isIdle = initialSnapshot.value === 'idle';
1146
- setIsLoading(!isIdle);
1147
964
  if (isIdle) {
965
+ flushSync(() => setIsLoading(false));
1148
966
  setError(null);
1149
967
  }
968
+ else {
969
+ setIsLoading(true);
970
+ }
1150
971
  // Subscribe to all status changes and update isLoading based on whether status is 'idle'
1151
972
  subscriptionRef.current = service.subscribe((snapshot) => {
1152
973
  const isIdle = snapshot.value === 'idle';
1153
- setIsLoading(!isIdle);
1154
974
  if (isIdle) {
975
+ flushSync(() => setIsLoading(false));
1155
976
  setError(null);
1156
977
  }
978
+ else {
979
+ setIsLoading(true);
980
+ }
1157
981
  });
1158
982
  }
1159
983
  catch (error) {
@@ -1197,17 +1021,19 @@ const useSchema = (schemaIdentifier) => {
1197
1021
  error,
1198
1022
  };
1199
1023
  };
1024
+ const SEED_SCHEMAS_QUERY_KEY = ['seed', 'schemas'];
1200
1025
  const useSchemas = () => {
1201
- const [schemas$1, setSchemas] = useState([]);
1202
- const [isLoading, setIsLoading] = useState(false);
1203
- const [error, setError] = useState(null);
1204
1026
  const isClientReady = useIsClientReady();
1205
- const subscriptionsRef = useRef(new Map());
1206
- const loadingSchemasRef = useRef(new Set());
1027
+ const queryClient = useQueryClient();
1207
1028
  const previousSchemasTableDataRef = useRef(undefined);
1208
- const schemasRef = useRef([]); // Track schemas for comparison without triggering effects
1209
- // Watch the schemas table for changes
1210
- // Memoize the query so it's stable across renders - this is critical for distinctUntilChanged to work
1029
+ const schemasRef = useRef([]);
1030
+ const { data: schemas$1 = [], isLoading, error: queryError, } = useQuery({
1031
+ queryKey: SEED_SCHEMAS_QUERY_KEY,
1032
+ queryFn: () => Schema.all({ waitForReady: true }),
1033
+ enabled: isClientReady,
1034
+ });
1035
+ schemasRef.current = schemas$1;
1036
+ // Watch the schemas table for changes and invalidate so useQuery refetches
1211
1037
  const db = isClientReady ? BaseDb.getAppDb() : null;
1212
1038
  const schemasQuery = useMemo(() => {
1213
1039
  if (!db)
@@ -1215,131 +1041,17 @@ const useSchemas = () => {
1215
1041
  return db.select().from(schemas).orderBy(schemas.name, desc(schemas.version));
1216
1042
  }, [db, isClientReady]);
1217
1043
  const schemasTableData = useLiveQuery(schemasQuery);
1218
- const fetchSchemas = useCallback(async () => {
1219
- try {
1220
- setIsLoading(true);
1221
- const timestamp = Date.now();
1222
- // Also check what's in the database directly before calling Schema.all()
1223
- const db = BaseDb.getAppDb();
1224
- if (db) {
1225
- const directCheck = await db.select().from(schemas).orderBy(schemas.name, desc(schemas.version));
1226
- }
1227
- const allSchemas = await Schema.all();
1228
- // Filter out schemas without an id and subscribe to state changes
1229
- const readySchemas = [];
1230
- const loadingSchemasList = [];
1231
- // Clear previous loading set
1232
- loadingSchemasRef.current.clear();
1233
- for (const schema of allSchemas) {
1234
- const snapshot = schema.getService().getSnapshot();
1235
- const hasId = !!schema.id;
1236
- const isIdle = snapshot.value === 'idle';
1237
- if (hasId && isIdle) {
1238
- // Schema is ready
1239
- readySchemas.push(schema);
1240
- }
1241
- else {
1242
- // Schema is still loading - subscribe to state changes
1243
- loadingSchemasList.push(schema);
1244
- loadingSchemasRef.current.add(schema);
1245
- // Clean up any existing subscription for this schema
1246
- const existingSub = subscriptionsRef.current.get(schema);
1247
- if (existingSub) {
1248
- existingSub.unsubscribe();
1249
- }
1250
- // Subscribe to state changes
1251
- const subscription = schema.getService().subscribe((snapshot) => {
1252
- const hasId = !!schema.id;
1253
- const isIdle = snapshot.value === 'idle';
1254
- if (hasId && isIdle) {
1255
- // Schema is now ready - update state
1256
- setSchemas(prev => {
1257
- // Check if schema is already in the list (by id)
1258
- if (schema.id && prev.some(s => s.id === schema.id)) {
1259
- return prev;
1260
- }
1261
- // Add the newly ready schema
1262
- const updated = [...prev, schema].sort((a, b) => {
1263
- // Sort by name for consistency
1264
- const nameA = a.metadata?.name || '';
1265
- const nameB = b.metadata?.name || '';
1266
- return nameA.localeCompare(nameB);
1267
- });
1268
- schemasRef.current = updated; // Update ref for comparison
1269
- return updated;
1270
- });
1271
- // Remove from loading set and clean up subscription
1272
- loadingSchemasRef.current.delete(schema);
1273
- subscription.unsubscribe();
1274
- subscriptionsRef.current.delete(schema);
1275
- // Update loading state based on remaining loading schemas
1276
- setIsLoading(loadingSchemasRef.current.size > 0);
1277
- }
1278
- else if (snapshot.value === 'error') {
1279
- // Schema failed to load - clean up subscription
1280
- loadingSchemasRef.current.delete(schema);
1281
- subscription.unsubscribe();
1282
- subscriptionsRef.current.delete(schema);
1283
- // Update loading state based on remaining loading schemas
1284
- setIsLoading(loadingSchemasRef.current.size > 0);
1285
- }
1286
- });
1287
- subscriptionsRef.current.set(schema, subscription);
1288
- }
1289
- }
1290
- // Set initial ready schemas
1291
- setSchemas(readySchemas);
1292
- schemasRef.current = readySchemas; // Update ref for comparison
1293
- setError(null);
1294
- setIsLoading(loadingSchemasList.length > 0); // Still loading if any schemas are loading
1295
- }
1296
- catch (error) {
1297
- setError(error);
1298
- setIsLoading(false);
1299
- }
1300
- }, []); // Remove schemasTableData dependency - we'll call fetchSchemas explicitly when table data changes
1301
- // Cleanup subscriptions for schemas that are no longer in the list
1302
- useEffect(() => {
1303
- const currentSchemaIds = new Set(schemas$1.map(s => s.id).filter(Boolean));
1304
- // Clean up subscriptions for schemas that are no longer in the list
1305
- for (const [schema, subscription] of subscriptionsRef.current.entries()) {
1306
- if (schema.id && !currentSchemaIds.has(schema.id)) {
1307
- // Schema is no longer in the list, clean up subscription
1308
- subscription.unsubscribe();
1309
- subscriptionsRef.current.delete(schema);
1310
- loadingSchemasRef.current.delete(schema);
1311
- }
1312
- }
1313
- // Update loading state based on remaining loading schemas
1314
- if (loadingSchemasRef.current.size === 0 && isLoading) {
1315
- setIsLoading(false);
1316
- }
1317
- }, [schemas$1, isLoading]);
1318
- // Fetch schemas on initial mount when client is ready
1319
- useEffect(() => {
1320
- if (!isClientReady) {
1321
- return;
1322
- }
1323
- // Initial fetch when client becomes ready
1324
- fetchSchemas();
1325
- }, [isClientReady, fetchSchemas]);
1326
- // Refetch schemas when table data actually changes (not just reference)
1327
1044
  useEffect(() => {
1328
1045
  if (!isClientReady || !schemasTableData) {
1329
1046
  return;
1330
1047
  }
1331
- // Check if schemasTableData actually changed by comparing with previous value
1332
1048
  const prevData = previousSchemasTableDataRef.current;
1333
1049
  const prevDataJson = prevData ? JSON.stringify(prevData) : 'undefined';
1334
1050
  const currDataJson = schemasTableData ? JSON.stringify(schemasTableData) : 'undefined';
1335
1051
  if (prevDataJson === currDataJson && prevData !== undefined) {
1336
- // Data hasn't actually changed, skip refetch
1337
1052
  return;
1338
1053
  }
1339
- // Update ref with current data
1340
1054
  previousSchemasTableDataRef.current = schemasTableData;
1341
- // Extract identifying information from current schemas in state (using ref to avoid dependency)
1342
- // Use schemaFileId if available, otherwise fall back to name+version
1343
1055
  const currentSchemasSet = new Set();
1344
1056
  for (const schema of schemasRef.current) {
1345
1057
  const schemaFileId = schema.id || schema.schemaFileId;
@@ -1347,7 +1059,6 @@ const useSchemas = () => {
1347
1059
  currentSchemasSet.add(schemaFileId);
1348
1060
  }
1349
1061
  else {
1350
- // Fallback to name+version if schemaFileId not available
1351
1062
  const name = schema.metadata?.name;
1352
1063
  const version = schema.version;
1353
1064
  if (name && version !== undefined) {
@@ -1355,73 +1066,109 @@ const useSchemas = () => {
1355
1066
  }
1356
1067
  }
1357
1068
  }
1358
- // Extract identifying information from schemasTableData
1359
1069
  const tableDataSchemasSet = new Set();
1360
1070
  for (const dbSchema of schemasTableData) {
1361
- // Skip internal Seed Protocol schema for comparison (it's filtered out by Schema.all())
1362
- if (dbSchema.name === 'Seed Protocol') {
1071
+ if (dbSchema.name === 'Seed Protocol')
1363
1072
  continue;
1364
- }
1365
1073
  if (dbSchema.schemaFileId) {
1366
1074
  tableDataSchemasSet.add(dbSchema.schemaFileId);
1367
1075
  }
1368
- else {
1369
- // Fallback to name+version if schemaFileId not available
1370
- if (dbSchema.name && dbSchema.version !== undefined) {
1371
- tableDataSchemasSet.add(`${dbSchema.name}:${dbSchema.version}`);
1372
- }
1076
+ else if (dbSchema.name != null && dbSchema.version !== undefined) {
1077
+ tableDataSchemasSet.add(`${dbSchema.name}:${dbSchema.version}`);
1373
1078
  }
1374
1079
  }
1375
- // Compare sets to detect changes
1376
1080
  const setsAreEqual = currentSchemasSet.size === tableDataSchemasSet.size &&
1377
- [...currentSchemasSet].every(id => tableDataSchemasSet.has(id));
1378
- if (setsAreEqual) {
1379
- // Schemas in state match table data, skip refetch
1380
- return;
1081
+ [...currentSchemasSet].every((id) => tableDataSchemasSet.has(id));
1082
+ if (!setsAreEqual) {
1083
+ queryClient.invalidateQueries({ queryKey: SEED_SCHEMAS_QUERY_KEY });
1381
1084
  }
1382
- // Schemas have changed, fetch updated schemas
1383
- fetchSchemas();
1384
- }, [isClientReady, schemasTableData, fetchSchemas]); // Removed 'schemas' from dependencies to break the loop
1385
- // Cleanup all subscriptions on unmount
1386
- useEffect(() => {
1387
- return () => {
1388
- subscriptionsRef.current.forEach(sub => sub.unsubscribe());
1389
- subscriptionsRef.current.clear();
1390
- loadingSchemasRef.current.clear();
1391
- };
1392
- }, []);
1085
+ }, [isClientReady, schemasTableData, queryClient]);
1393
1086
  return {
1394
1087
  schemas: schemas$1,
1395
1088
  isLoading,
1396
- error,
1089
+ error: queryError,
1397
1090
  };
1398
1091
  };
1399
1092
  const useCreateSchema = () => {
1400
- const errorRef = useRef(null);
1401
1093
  const subscriptionRef = useRef(null);
1402
1094
  const [isLoading, setIsLoading] = useState(false);
1403
1095
  const [error, setError] = useState(null);
1096
+ const resetError = useCallback(() => setError(null), []);
1404
1097
  const createSchema = useCallback((schemaName) => {
1098
+ setError(null);
1405
1099
  setIsLoading(true);
1406
- const schema = Schema.create(schemaName);
1100
+ subscriptionRef.current?.unsubscribe();
1101
+ subscriptionRef.current = null;
1102
+ const schema = Schema.create(schemaName, {
1103
+ waitForReady: false,
1104
+ });
1407
1105
  const subscription = schema.getService().subscribe((snapshot) => {
1408
1106
  if (snapshot.value === 'error') {
1409
- errorRef.current = new Error('Failed to create schema');
1107
+ const err = snapshot.context._loadingError?.error;
1108
+ setError(err instanceof Error ? err : new Error('Failed to create schema'));
1109
+ setIsLoading(false);
1410
1110
  }
1411
1111
  if (snapshot.value === 'idle') {
1112
+ setError(null);
1412
1113
  setIsLoading(false);
1413
1114
  }
1414
1115
  });
1415
1116
  subscriptionRef.current = subscription;
1416
1117
  return schema;
1417
- }, [setIsLoading]);
1118
+ }, []);
1418
1119
  useEffect(() => {
1419
- setError(errorRef.current);
1420
- }, [errorRef.current]);
1120
+ return () => {
1121
+ subscriptionRef.current?.unsubscribe();
1122
+ subscriptionRef.current = null;
1123
+ };
1124
+ }, []);
1421
1125
  return {
1422
1126
  createSchema,
1423
1127
  isLoading,
1424
1128
  error,
1129
+ resetError,
1130
+ };
1131
+ };
1132
+ const useDestroySchema = () => {
1133
+ const [currentInstance, setCurrentInstance] = useState(null);
1134
+ const [destroyState, setDestroyState] = useState({
1135
+ isLoading: false,
1136
+ error: null,
1137
+ });
1138
+ useEffect(() => {
1139
+ if (!currentInstance) {
1140
+ setDestroyState({ isLoading: false, error: null });
1141
+ return;
1142
+ }
1143
+ const service = currentInstance.getService();
1144
+ const update = () => {
1145
+ const snap = service.getSnapshot();
1146
+ const ctx = snap.context;
1147
+ setDestroyState({
1148
+ isLoading: !!ctx._destroyInProgress,
1149
+ error: ctx._destroyError ? new Error(ctx._destroyError.message) : null,
1150
+ });
1151
+ };
1152
+ update();
1153
+ const sub = service.subscribe(update);
1154
+ return () => sub.unsubscribe();
1155
+ }, [currentInstance]);
1156
+ const destroy = useCallback(async (schema) => {
1157
+ if (!schema)
1158
+ return;
1159
+ setCurrentInstance(schema);
1160
+ await schema.destroy();
1161
+ }, []);
1162
+ const resetError = useCallback(() => {
1163
+ if (currentInstance) {
1164
+ currentInstance.getService().send({ type: 'clearDestroyError' });
1165
+ }
1166
+ }, [currentInstance]);
1167
+ return {
1168
+ destroy,
1169
+ isLoading: destroyState.isLoading,
1170
+ error: destroyState.error,
1171
+ resetError,
1425
1172
  };
1426
1173
  };
1427
1174
  const useAllSchemaVersions = () => {
@@ -1454,7 +1201,9 @@ const useAllSchemaVersions = () => {
1454
1201
  }
1455
1202
  else {
1456
1203
  // Create new instance
1457
- const schema = Schema.create(schemaName);
1204
+ const schema = Schema.create(schemaName, {
1205
+ waitForReady: false,
1206
+ });
1458
1207
  currentInstances.set(schemaName, schema);
1459
1208
  }
1460
1209
  }
@@ -1498,18 +1247,79 @@ const useAllSchemaVersions = () => {
1498
1247
  * @param schemaId - The schema ID (schema file ID) or schema name to get models from
1499
1248
  * @returns Array of Model instances belonging to the schema
1500
1249
  */
1250
+ const getModelsQueryKey = (schemaId) => ['seed', 'models', schemaId];
1251
+ // Last-known-good models per schemaId so we never flash [] after remount or refetch race (survives component unmount).
1252
+ const lastModelsBySchemaId = new Map();
1501
1253
  const useModels = (schemaId) => {
1502
- const [models$1, setModels] = useState([]);
1503
- const [isLoading, setIsLoading] = useState(false);
1504
- const [error, setError] = useState(null);
1505
1254
  const isClientReady = useIsClientReady();
1506
- // Watch the models table for changes via model_schemas join table
1507
- // Memoize the query so it's stable across renders - this is critical for distinctUntilChanged to work
1508
- const db = isClientReady ? BaseDb.getAppDb() : null;
1509
- const modelsQuery = useMemo(() => {
1510
- if (!db || !schemaId)
1255
+ const queryClient = useQueryClient();
1256
+ const modelsRef = useRef([]);
1257
+ const queryKey = useMemo(() => getModelsQueryKey(schemaId), [schemaId]);
1258
+ const { data: models$1 = [], isLoading, error: queryError, } = useQuery({
1259
+ queryKey,
1260
+ queryFn: async () => {
1261
+ // Capture previous data before any async work so we don't overwrite good cache with [].
1262
+ const prev = queryClient.getQueryData(queryKey);
1263
+ // Use waitForReady: false so we return models as soon as they exist; waitForReady: true
1264
+ // filters to only idle models and can return [] while a model is in creatingProperties.
1265
+ const next = await Model.all(schemaId, { waitForReady: false });
1266
+ // getModelsData can intermittently return [] after returning data; keep prev to avoid overwrite.
1267
+ if (Array.isArray(prev) && prev.length > 0 && Array.isArray(next) && next.length === 0) {
1268
+ return [...prev];
1269
+ }
1270
+ // If this refetch returned [], avoid overwriting non-empty cache (e.g. race where another refetch already wrote data).
1271
+ if (Array.isArray(next) && next.length === 0) {
1272
+ const current = queryClient.getQueryData(queryKey);
1273
+ if (Array.isArray(current) && current.length > 0) {
1274
+ return [...current];
1275
+ }
1276
+ }
1277
+ return next;
1278
+ },
1279
+ enabled: isClientReady && !!schemaId,
1280
+ });
1281
+ // Never expose [] when we previously had models (avoids flash from refetch races, remounts, or intermittent getModelsData returning []).
1282
+ // When schemaId is null/undefined, always show [] — do not use fallback from a previous schema.
1283
+ const schemaIdKey = schemaId && typeof schemaId === 'string' ? schemaId : '';
1284
+ if (models$1.length > 0) {
1285
+ lastModelsBySchemaId.set(schemaIdKey, models$1);
1286
+ }
1287
+ const fallback = modelsRef.current.length > 0 ? modelsRef.current : lastModelsBySchemaId.get(schemaIdKey);
1288
+ const displayModels = !schemaId
1289
+ ? models$1
1290
+ : models$1.length > 0
1291
+ ? models$1
1292
+ : fallback?.length
1293
+ ? fallback
1294
+ : models$1;
1295
+ modelsRef.current = displayModels;
1296
+ // When a model is created, writeModelToDb posts to this channel; live query over join often doesn't re-run
1297
+ useEffect(() => {
1298
+ if (!schemaId || typeof BroadcastChannel === 'undefined')
1299
+ return;
1300
+ const ch = new BroadcastChannel('seed-models-invalidate');
1301
+ const onMessage = (event) => {
1302
+ const { schemaName, schemaFileId } = event.data || {};
1303
+ if (schemaId === schemaName || schemaId === schemaFileId) {
1304
+ queryClient.invalidateQueries({ queryKey });
1305
+ queryClient.refetchQueries({ queryKey });
1306
+ }
1307
+ };
1308
+ ch.addEventListener('message', onMessage);
1309
+ return () => {
1310
+ ch.removeEventListener('message', onMessage);
1311
+ ch.close();
1312
+ };
1313
+ }, [schemaId, queryClient, queryKey]);
1314
+ // Stabilize query reference: only recreate when (schemaId, isClientReady) change, not when db reference changes.
1315
+ // This keeps the same liveQuery observable/subscription alive so effects can deliver updates when a new model is added.
1316
+ const stableModelsQueryKeyRef = useRef(null);
1317
+ const stableModelsQueryRef = useRef(null);
1318
+ function buildModelsQuery() {
1319
+ const currentDb = BaseDb.getAppDb();
1320
+ if (!currentDb || !schemaId)
1511
1321
  return null;
1512
- return db
1322
+ return currentDb
1513
1323
  .select({
1514
1324
  modelFileId: models.schemaFileId,
1515
1325
  modelName: models.name,
@@ -1518,113 +1328,59 @@ const useModels = (schemaId) => {
1518
1328
  .innerJoin(modelSchemas, eq(schemas.id, modelSchemas.schemaId))
1519
1329
  .innerJoin(models, eq(modelSchemas.modelId, models.id))
1520
1330
  .where(or(eq(schemas.schemaFileId, schemaId), eq(schemas.name, schemaId)));
1521
- }, [db, isClientReady, schemaId]);
1331
+ }
1332
+ const modelsQuery = useMemo(() => {
1333
+ if (!schemaId || !isClientReady)
1334
+ return null;
1335
+ const key = { schemaId, ready: isClientReady };
1336
+ const prevKey = stableModelsQueryKeyRef.current;
1337
+ if (prevKey &&
1338
+ prevKey.schemaId === key.schemaId &&
1339
+ prevKey.ready === key.ready &&
1340
+ stableModelsQueryRef.current !== null) {
1341
+ return stableModelsQueryRef.current;
1342
+ }
1343
+ const q = buildModelsQuery();
1344
+ if (!q)
1345
+ return null;
1346
+ stableModelsQueryKeyRef.current = key;
1347
+ stableModelsQueryRef.current = q;
1348
+ return q;
1349
+ }, [schemaId, isClientReady]);
1522
1350
  const modelsTableData = useLiveQuery(modelsQuery);
1523
- const fetchModels = useCallback(async () => {
1524
- if (!schemaId) {
1525
- setModels([]);
1526
- setIsLoading(false);
1527
- setError(null);
1528
- return;
1529
- }
1530
- try {
1531
- setIsLoading(true);
1532
- const timestamp = Date.now();
1533
- console.log(`[useModels.fetchModels] [${timestamp}] Starting fetch, modelsTableData count:`, modelsTableData?.length, 'models:', modelsTableData?.map(m => m.modelName));
1534
- // Use Model.createBySchemaId to get Model instances (handles caching)
1535
- const modelInstances = await Model.createBySchemaId(schemaId);
1536
- console.log(`[useModels.fetchModels] [${timestamp}] Model.createBySchemaId() returned:`, modelInstances.length, 'models:', modelInstances.map((m) => m.modelName));
1537
- setModels(prev => {
1538
- // Check if anything actually changed
1539
- if (prev.length !== modelInstances.length) {
1540
- console.log('[useModels] Length changed:', prev.length, '->', modelInstances.length);
1541
- return modelInstances;
1542
- }
1543
- // Compare by modelFileId (schemaFileId) or name
1544
- const hasChanged = modelInstances.some((model, i) => !prev[i] ||
1545
- model.id !== prev[i].id ||
1546
- model.modelName !== prev[i].modelName);
1547
- if (hasChanged) {
1548
- console.log('[useModels] Models changed (by ID or name)');
1549
- }
1550
- else {
1551
- console.log('[useModels] No changes detected');
1552
- }
1553
- return hasChanged ? modelInstances : prev;
1554
- });
1555
- setError(null);
1556
- setIsLoading(false);
1557
- }
1558
- catch (error) {
1559
- setError(error);
1560
- setIsLoading(false);
1561
- }
1562
- }, [schemaId]);
1563
- // Fetch models on initial mount when client is ready
1564
1351
  useEffect(() => {
1565
- if (!isClientReady) {
1352
+ if (!isClientReady || !modelsTableData || !schemaId)
1566
1353
  return;
1567
- }
1568
- // Initial fetch when client becomes ready
1569
- fetchModels();
1570
- }, [isClientReady, fetchModels]);
1571
- // Refetch models when table data actually changes (not just reference)
1572
- useEffect(() => {
1573
- if (!isClientReady || !modelsTableData || !schemaId) {
1574
- return;
1575
- }
1576
- // Extract identifying information from current models in state
1577
- // Use modelFileId (schemaFileId) if available, otherwise fall back to name
1578
1354
  const currentModelsSet = new Set();
1579
- for (const model of models$1) {
1355
+ for (const model of modelsRef.current) {
1580
1356
  const modelFileId = model.id || model.modelFileId;
1581
- if (modelFileId) {
1357
+ if (modelFileId)
1582
1358
  currentModelsSet.add(modelFileId);
1583
- }
1584
- else {
1585
- // Fallback to name if modelFileId not available
1586
- const name = model.modelName;
1587
- if (name) {
1588
- currentModelsSet.add(name);
1589
- }
1590
- }
1359
+ else if (model.modelName)
1360
+ currentModelsSet.add(model.modelName);
1591
1361
  }
1592
- // Extract identifying information from modelsTableData
1593
1362
  const tableDataModelsSet = new Set();
1594
1363
  for (const dbModel of modelsTableData) {
1595
- if (dbModel.modelFileId) {
1364
+ if (dbModel.modelFileId)
1596
1365
  tableDataModelsSet.add(dbModel.modelFileId);
1597
- }
1598
- else {
1599
- // Fallback to name if modelFileId not available
1600
- if (dbModel.modelName) {
1601
- tableDataModelsSet.add(dbModel.modelName);
1602
- }
1603
- }
1366
+ else if (dbModel.modelName)
1367
+ tableDataModelsSet.add(dbModel.modelName);
1604
1368
  }
1605
- // Compare sets to detect changes
1606
1369
  const setsAreEqual = currentModelsSet.size === tableDataModelsSet.size &&
1607
- [...currentModelsSet].every(id => tableDataModelsSet.has(id));
1608
- if (setsAreEqual) {
1609
- // Models in state match table data, skip refetch
1610
- return;
1611
- }
1612
- // Models have changed - log for debugging
1613
- console.log('[useModels] modelsTableData changed:', {
1614
- currentCount: currentModelsSet.size,
1615
- tableDataCount: tableDataModelsSet.size,
1616
- currentIds: Array.from(currentModelsSet),
1617
- tableDataIds: Array.from(tableDataModelsSet),
1618
- tableDataNames: modelsTableData.map(m => m.modelName),
1619
- tableDataFull: modelsTableData.map(m => ({ name: m.modelName, modelFileId: m.modelFileId })),
1620
- });
1621
- // Models have changed, fetch updated models
1622
- fetchModels();
1623
- }, [isClientReady, modelsTableData, models$1, fetchModels, schemaId]);
1370
+ [...currentModelsSet].every((id) => tableDataModelsSet.has(id));
1371
+ // Only invalidate when the table has rows we might be missing (live query saw new data).
1372
+ // Do NOT invalidate when we have more than the table: the live query may not have updated
1373
+ // yet (e.g. join over model_schemas), and refetching would overwrite cache with [].
1374
+ const tableHasNewRows = tableDataModelsSet.size > 0 &&
1375
+ [...tableDataModelsSet].some((id) => !currentModelsSet.has(id));
1376
+ if (!setsAreEqual && tableHasNewRows) {
1377
+ queryClient.invalidateQueries({ queryKey });
1378
+ }
1379
+ }, [isClientReady, modelsTableData, schemaId, queryClient, queryKey]);
1624
1380
  return {
1625
- models: models$1,
1381
+ models: displayModels,
1626
1382
  isLoading,
1627
- error,
1383
+ error: queryError,
1628
1384
  };
1629
1385
  };
1630
1386
  /**
@@ -1754,6 +1510,89 @@ const useModel = (schemaIdOrModelId, modelName) => {
1754
1510
  error: modelsError,
1755
1511
  };
1756
1512
  };
1513
+ const useCreateModel = () => {
1514
+ const subscriptionRef = useRef(undefined);
1515
+ const [isLoading, setIsLoading] = useState(false);
1516
+ const [error, setError] = useState(null);
1517
+ const resetError = useCallback(() => setError(null), []);
1518
+ const create = useCallback((schemaName, modelName, options) => {
1519
+ setError(null);
1520
+ setIsLoading(true);
1521
+ subscriptionRef.current?.unsubscribe();
1522
+ subscriptionRef.current = undefined;
1523
+ const model = Model.create(modelName, schemaName, {
1524
+ ...options,
1525
+ waitForReady: false,
1526
+ });
1527
+ const subscription = model.getService().subscribe((snapshot) => {
1528
+ if (snapshot.value === 'error') {
1529
+ setError(snapshot.context._loadingError?.error ??
1530
+ new Error('Failed to create model'));
1531
+ setIsLoading(false);
1532
+ }
1533
+ if (snapshot.value === 'idle') {
1534
+ setError(null);
1535
+ setIsLoading(false);
1536
+ }
1537
+ });
1538
+ subscriptionRef.current = subscription;
1539
+ return model;
1540
+ }, []);
1541
+ useEffect(() => {
1542
+ return () => {
1543
+ subscriptionRef.current?.unsubscribe();
1544
+ subscriptionRef.current = undefined;
1545
+ };
1546
+ }, []);
1547
+ return {
1548
+ create,
1549
+ isLoading,
1550
+ error,
1551
+ resetError,
1552
+ };
1553
+ };
1554
+ const useDestroyModel = () => {
1555
+ const [currentInstance, setCurrentInstance] = useState(null);
1556
+ const [destroyState, setDestroyState] = useState({
1557
+ isLoading: false,
1558
+ error: null,
1559
+ });
1560
+ useEffect(() => {
1561
+ if (!currentInstance) {
1562
+ setDestroyState({ isLoading: false, error: null });
1563
+ return;
1564
+ }
1565
+ const service = currentInstance.getService();
1566
+ const update = () => {
1567
+ const snap = service.getSnapshot();
1568
+ const ctx = snap.context;
1569
+ setDestroyState({
1570
+ isLoading: !!ctx._destroyInProgress,
1571
+ error: ctx._destroyError ? new Error(ctx._destroyError.message) : null,
1572
+ });
1573
+ };
1574
+ update();
1575
+ const sub = service.subscribe(update);
1576
+ return () => sub.unsubscribe();
1577
+ }, [currentInstance]);
1578
+ const destroy = useCallback(async (model) => {
1579
+ if (!model)
1580
+ return;
1581
+ setCurrentInstance(model);
1582
+ await model.destroy();
1583
+ }, []);
1584
+ const resetError = useCallback(() => {
1585
+ if (currentInstance) {
1586
+ currentInstance.getService().send({ type: 'clearDestroyError' });
1587
+ }
1588
+ }, [currentInstance]);
1589
+ return {
1590
+ destroy,
1591
+ isLoading: destroyState.isLoading,
1592
+ error: destroyState.error,
1593
+ resetError,
1594
+ };
1595
+ };
1757
1596
 
1758
1597
  debug('seedSdk:browser:react:modelProperty');
1759
1598
  /**
@@ -1773,7 +1612,7 @@ const useModelProperties = (schemaIdOrModelId, modelName) => {
1773
1612
  // Use useModel to handle both lookup patterns (by ID or by schemaId + modelName)
1774
1613
  const { model } = useModel(schemaIdOrModelId, modelName);
1775
1614
  // Determine the modelName for use in getPropertySchema
1776
- const modelNameForProperty = useMemo(() => {
1615
+ useMemo(() => {
1777
1616
  if (!model)
1778
1617
  return undefined;
1779
1618
  try {
@@ -1783,24 +1622,27 @@ const useModelProperties = (schemaIdOrModelId, modelName) => {
1783
1622
  return undefined;
1784
1623
  }
1785
1624
  }, [model]);
1786
- const [modelProperties, setModelProperties] = useState([]);
1787
- const [isLoading, setIsLoading] = useState(false);
1788
- const [error, setError] = useState(null);
1789
1625
  const isClientReady = useIsClientReady();
1626
+ const queryClient = useQueryClient();
1790
1627
  // Get _dbId (database ID) from model context
1791
1628
  const dbModelId = useMemo(() => {
1792
1629
  if (!model)
1793
1630
  return null;
1794
1631
  try {
1795
1632
  const context = model._getSnapshotContext();
1796
- return context._dbId; // _dbId is the database integer ID
1633
+ return context._dbId;
1797
1634
  }
1798
1635
  catch {
1799
1636
  return null;
1800
1637
  }
1801
1638
  }, [model]);
1802
- // Watch the properties table for changes
1803
- // Memoize the query so it's stable across renders - this is critical for distinctUntilChanged to work
1639
+ const modelId = model?.id;
1640
+ const modelPropertiesQueryKey = useMemo(() => ['seed', 'modelProperties', modelId ?? ''], [modelId]);
1641
+ const { data: modelProperties = [], isLoading, error: queryError, } = useQuery({
1642
+ queryKey: modelPropertiesQueryKey,
1643
+ queryFn: () => ModelProperty.all(modelId, { waitForReady: true }),
1644
+ enabled: isClientReady && !!modelId,
1645
+ });
1804
1646
  const db = isClientReady ? BaseDb.getAppDb() : null;
1805
1647
  const propertiesQuery = useMemo(() => {
1806
1648
  if (!db || !dbModelId)
@@ -1816,159 +1658,54 @@ const useModelProperties = (schemaIdOrModelId, modelName) => {
1816
1658
  .where(eq(properties.modelId, dbModelId));
1817
1659
  }, [db, isClientReady, dbModelId]);
1818
1660
  const propertiesTableData = useLiveQuery(propertiesQuery);
1819
- const fetchModelProperties = useCallback(async () => {
1820
- if (!modelNameForProperty || !model || !schemaIdOrModelId) {
1821
- setModelProperties([]);
1822
- setIsLoading(false);
1823
- setError(null);
1824
- return;
1825
- }
1826
- try {
1827
- setIsLoading(true);
1828
- const timestamp = Date.now();
1829
- console.log(`[useModelProperties.fetchModelProperties] [${timestamp}] Starting fetch, propertiesTableData count:`, propertiesTableData?.length, 'properties:', propertiesTableData?.map(p => p.name));
1830
- // Use propertiesTableData (database state) as the source of truth instead of model.properties (schema file)
1831
- // This ensures we get the current names even after property renames
1832
- if (!propertiesTableData || propertiesTableData.length === 0) {
1833
- setModelProperties([]);
1834
- setError(null);
1835
- setIsLoading(false);
1836
- return;
1837
- }
1838
- const _modelProperties = [];
1839
- // Iterate over propertiesTableData and create ModelProperty instances by schemaFileId
1840
- // This works even when property names have changed, since schemaFileId is stable
1841
- for (const dbProperty of propertiesTableData) {
1842
- if (!dbProperty.schemaFileId) {
1843
- // If no schemaFileId, fall back to name-based lookup (for backwards compatibility)
1844
- const modelPropertyData = await getPropertySchema(modelNameForProperty, dbProperty.name);
1845
- if (modelPropertyData) {
1846
- const modelProperty = ModelProperty.create({
1847
- ...modelPropertyData,
1848
- });
1849
- _modelProperties.push(modelProperty);
1850
- }
1851
- }
1852
- else {
1853
- // Use createById to get/create the instance by schemaFileId (stable across renames)
1854
- const modelProperty = await ModelProperty.createById(dbProperty.schemaFileId);
1855
- if (modelProperty) {
1856
- _modelProperties.push(modelProperty);
1857
- }
1858
- }
1859
- }
1860
- console.log(`[useModelProperties.fetchModelProperties] [${timestamp}] Created ${_modelProperties.length} ModelProperty instances`);
1861
- setModelProperties(prev => {
1862
- // Check if anything actually changed
1863
- if (prev.length !== _modelProperties.length) {
1864
- console.log('[useModelProperties] Length changed:', prev.length, '->', _modelProperties.length);
1865
- return _modelProperties;
1866
- }
1867
- // Compare by property name or schemaFileId
1868
- const hasChanged = _modelProperties.some((prop, i) => {
1869
- if (!prev[i])
1870
- return true;
1871
- const prevContext = prev[i]._getSnapshotContext();
1872
- const currContext = prop._getSnapshotContext();
1873
- const prevId = prevContext?.id;
1874
- const currId = currContext?.id;
1875
- const prevName = prev[i].name;
1876
- const currName = prop.name;
1877
- return prevId !== currId || prevName !== currName;
1878
- });
1879
- if (hasChanged) {
1880
- console.log('[useModelProperties] Properties changed (by ID or name)');
1881
- }
1882
- else {
1883
- console.log('[useModelProperties] No changes detected');
1884
- }
1885
- return hasChanged ? _modelProperties : prev;
1886
- });
1887
- setError(null);
1888
- setIsLoading(false);
1889
- }
1890
- catch (error) {
1891
- setError(error);
1892
- setIsLoading(false);
1893
- }
1894
- }, [modelNameForProperty, model, propertiesTableData, schemaIdOrModelId]);
1895
- // Fetch model properties when dbModelId becomes available (model has finished loading)
1896
- // This ensures we wait for the model to be fully loaded before trying to fetch properties
1661
+ const modelPropertiesRef = useRef([]);
1662
+ modelPropertiesRef.current = modelProperties;
1663
+ // Fallback: when we have modelId but query returned [] (e.g. properties not in DB yet or
1664
+ // propertiesTableData is undefined because model._dbId isn't set yet), schedule refetches
1665
+ // so we pick up properties after they're written.
1897
1666
  useEffect(() => {
1898
- if (!isClientReady || !dbModelId || !modelNameForProperty || !model || !schemaIdOrModelId) {
1667
+ if (!modelId || modelProperties.length > 0 || !queryClient || !modelPropertiesQueryKey)
1899
1668
  return;
1900
- }
1901
- // Wait for propertiesTableData to be available before initial fetch
1902
- // (it may be undefined initially while the query is starting)
1903
- if (propertiesTableData === undefined) {
1904
- return;
1905
- }
1906
- // Initial fetch when model is ready and dbModelId is available
1907
- fetchModelProperties();
1908
- }, [isClientReady, dbModelId, fetchModelProperties, modelNameForProperty, model, schemaIdOrModelId, propertiesTableData]);
1909
- // Refetch model properties when table data actually changes (not just reference)
1669
+ const delays = [400, 1200, 2500];
1670
+ const timers = delays.map((ms) => setTimeout(() => {
1671
+ queryClient.invalidateQueries({ queryKey: modelPropertiesQueryKey });
1672
+ }, ms));
1673
+ return () => timers.forEach((t) => clearTimeout(t));
1674
+ }, [modelId, modelProperties.length, queryClient, modelPropertiesQueryKey]);
1910
1675
  useEffect(() => {
1911
- if (!isClientReady || !modelNameForProperty || !model || !schemaIdOrModelId || !dbModelId) {
1912
- return;
1913
- }
1914
- // If propertiesTableData is undefined, the query hasn't started yet - wait for it
1915
- if (propertiesTableData === undefined) {
1676
+ if (!isClientReady || !model?.id || !propertiesTableData || !modelPropertiesQueryKey)
1916
1677
  return;
1917
- }
1918
- // Extract identifying information from current properties in state
1919
1678
  const currentPropertiesSet = new Set();
1920
- for (const prop of modelProperties) {
1679
+ for (const prop of modelPropertiesRef.current) {
1921
1680
  const context = prop._getSnapshotContext();
1922
1681
  const propertyFileId = context?.id;
1923
- if (propertyFileId) {
1682
+ if (propertyFileId)
1924
1683
  currentPropertiesSet.add(propertyFileId);
1925
- }
1926
- else {
1927
- // Fallback to name if propertyFileId not available
1928
- const name = prop.name;
1929
- if (name) {
1930
- currentPropertiesSet.add(name);
1931
- }
1932
- }
1684
+ else if (prop.name)
1685
+ currentPropertiesSet.add(prop.name);
1933
1686
  }
1934
- // Extract identifying information from propertiesTableData
1935
1687
  const tableDataPropertiesSet = new Set();
1936
1688
  for (const dbProperty of propertiesTableData) {
1937
- if (dbProperty.schemaFileId) {
1689
+ if (dbProperty.schemaFileId)
1938
1690
  tableDataPropertiesSet.add(dbProperty.schemaFileId);
1939
- }
1940
- else {
1941
- // Fallback to name if schemaFileId not available
1942
- if (dbProperty.name) {
1943
- tableDataPropertiesSet.add(dbProperty.name);
1944
- }
1945
- }
1691
+ else if (dbProperty.name)
1692
+ tableDataPropertiesSet.add(dbProperty.name);
1946
1693
  }
1947
- // Compare sets to detect changes
1948
- // If currentPropertiesSet is empty but tableDataPropertiesSet has data, that's a change
1949
1694
  const setsAreEqual = currentPropertiesSet.size === tableDataPropertiesSet.size &&
1950
- currentPropertiesSet.size > 0 &&
1951
- [...currentPropertiesSet].every(id => tableDataPropertiesSet.has(id));
1952
- if (setsAreEqual) {
1953
- // Properties in state match table data, skip refetch
1954
- return;
1955
- }
1956
- // Properties have changed - log for debugging
1957
- console.log('[useModelProperties] propertiesTableData changed:', {
1958
- currentCount: currentPropertiesSet.size,
1959
- tableDataCount: tableDataPropertiesSet.size,
1960
- currentIds: Array.from(currentPropertiesSet),
1961
- tableDataIds: Array.from(tableDataPropertiesSet),
1962
- tableDataNames: propertiesTableData.map(p => p.name),
1963
- tableDataFull: propertiesTableData.map(p => ({ name: p.name, schemaFileId: p.schemaFileId })),
1964
- });
1965
- // Properties have changed, fetch updated properties
1966
- fetchModelProperties();
1967
- }, [isClientReady, propertiesTableData, modelProperties, fetchModelProperties, modelNameForProperty, model, schemaIdOrModelId]);
1695
+ (currentPropertiesSet.size === 0 ||
1696
+ [...currentPropertiesSet].every((id) => tableDataPropertiesSet.has(id)));
1697
+ // Invalidate when cached list is out of sync with the table: either we have stale cached data
1698
+ // (currentPropertiesSet.size > 0) or the table has new rows we don't have yet (tableDataPropertiesSet.size > 0).
1699
+ // The latter handles the case where the initial query returned [] before properties were written.
1700
+ const shouldInvalidate = !setsAreEqual && (currentPropertiesSet.size > 0 || tableDataPropertiesSet.size > 0);
1701
+ if (shouldInvalidate) {
1702
+ queryClient.invalidateQueries({ queryKey: modelPropertiesQueryKey });
1703
+ }
1704
+ }, [isClientReady, propertiesTableData, model?.id, queryClient, modelPropertiesQueryKey]);
1968
1705
  return {
1969
1706
  modelProperties,
1970
1707
  isLoading,
1971
- error,
1708
+ error: queryError,
1972
1709
  };
1973
1710
  };
1974
1711
  /**
@@ -2098,13 +1835,13 @@ function useModelProperty(arg1, arg2, arg3) {
2098
1835
  resolvedModelName = lookupMode.modelName;
2099
1836
  }
2100
1837
  if (propertyData && resolvedModelName) {
2101
- const createdProperty = ModelProperty.create({
2102
- ...propertyData,
2103
- modelName: resolvedModelName,
1838
+ const createdProperty = ModelProperty.create({ ...propertyData, modelName: resolvedModelName }, { waitForReady: false });
1839
+ const resolvedProperty = createdProperty instanceof Promise ? await createdProperty : createdProperty;
1840
+ flushSync(() => {
1841
+ setModelProperty(resolvedProperty);
1842
+ setIsLoading(false);
1843
+ setError(null);
2104
1844
  });
2105
- setModelProperty(createdProperty);
2106
- setIsLoading(false);
2107
- setError(null);
2108
1845
  }
2109
1846
  else {
2110
1847
  setModelProperty(undefined);
@@ -2129,9 +1866,12 @@ function useModelProperty(arg1, arg2, arg3) {
2129
1866
  }
2130
1867
  updateModelProperty();
2131
1868
  }, [shouldLoad, updateModelProperty]);
2132
- // Subscribe to service changes when modelProperty is available
1869
+ // Subscribe to service changes when modelProperty is available.
1870
+ // Skip subscription for schemaId/modelFileId lookups where we created the instance locally—
1871
+ // refetching on every snapshot would set loading and can race with the initial render.
1872
+ const shouldSubscribe = lookupMode.type === 'propertyFileId';
2133
1873
  useEffect(() => {
2134
- if (!modelProperty) {
1874
+ if (!modelProperty || !shouldSubscribe) {
2135
1875
  return;
2136
1876
  }
2137
1877
  // Clean up previous subscription
@@ -2145,51 +1885,277 @@ function useModelProperty(arg1, arg2, arg3) {
2145
1885
  subscriptionRef.current?.unsubscribe();
2146
1886
  subscriptionRef.current = undefined;
2147
1887
  };
2148
- }, [modelProperty, updateModelProperty]);
1888
+ }, [modelProperty, updateModelProperty, shouldSubscribe]);
2149
1889
  return {
2150
1890
  modelProperty,
2151
1891
  isLoading,
2152
1892
  error,
2153
1893
  };
2154
1894
  }
2155
-
2156
- const deleteItem = async ({ seedLocalId, seedUid }) => {
2157
- const appDb = BaseDb.getAppDb();
2158
- const conditions = [];
2159
- if (seedLocalId) {
2160
- conditions.push(eq(seeds.localId, seedLocalId));
2161
- }
2162
- if (seedUid) {
2163
- conditions.push(eq(seeds.uid, seedUid));
2164
- }
2165
- if (conditions.length === 0) {
2166
- return;
2167
- }
2168
- await appDb
2169
- .update(seeds)
2170
- .set({
2171
- _markedForDeletion: 1,
2172
- })
2173
- .where(or(...conditions));
1895
+ /**
1896
+ * Hook to create a ModelProperty with loading and error state.
1897
+ * create(schemaId, modelName, property) creates a new property on the model.
1898
+ */
1899
+ const useCreateModelProperty = () => {
1900
+ const subscriptionRef = useRef(undefined);
1901
+ const [isLoading, setIsLoading] = useState(false);
1902
+ const [error, setError] = useState(null);
1903
+ const resetError = useCallback(() => setError(null), []);
1904
+ const create = useCallback((_schemaId, modelName, property) => {
1905
+ setError(null);
1906
+ setIsLoading(true);
1907
+ subscriptionRef.current?.unsubscribe();
1908
+ subscriptionRef.current = undefined;
1909
+ if (!modelName || !property.name || !property.dataType) {
1910
+ const err = new Error('modelName, property name and dataType are required');
1911
+ setError(err);
1912
+ setIsLoading(false);
1913
+ throw err;
1914
+ }
1915
+ const created = ModelProperty.create({ ...property, modelName }, { waitForReady: false });
1916
+ const subscription = created.getService().subscribe((snapshot) => {
1917
+ if (snapshot.value === 'error') {
1918
+ const err = snapshot.context._loadingError?.error ?? new Error('Failed to create model property');
1919
+ setError(err instanceof Error ? err : new Error(String(err)));
1920
+ setIsLoading(false);
1921
+ }
1922
+ if (snapshot.value === 'idle') {
1923
+ setError(null);
1924
+ setIsLoading(false);
1925
+ }
1926
+ });
1927
+ subscriptionRef.current = subscription;
1928
+ return created;
1929
+ }, []);
1930
+ useEffect(() => {
1931
+ return () => {
1932
+ subscriptionRef.current?.unsubscribe();
1933
+ subscriptionRef.current = undefined;
1934
+ };
1935
+ }, []);
1936
+ return {
1937
+ create,
1938
+ isLoading,
1939
+ error,
1940
+ resetError,
1941
+ };
1942
+ };
1943
+ const useDestroyModelProperty = () => {
1944
+ const [currentInstance, setCurrentInstance] = useState(null);
1945
+ const [destroyState, setDestroyState] = useState({
1946
+ isLoading: false,
1947
+ error: null,
1948
+ });
1949
+ useEffect(() => {
1950
+ if (!currentInstance) {
1951
+ setDestroyState({ isLoading: false, error: null });
1952
+ return;
1953
+ }
1954
+ const service = currentInstance.getService();
1955
+ const update = () => {
1956
+ const snap = service.getSnapshot();
1957
+ const ctx = snap.context;
1958
+ setDestroyState({
1959
+ isLoading: !!ctx._destroyInProgress,
1960
+ error: ctx._destroyError ? new Error(ctx._destroyError.message) : null,
1961
+ });
1962
+ };
1963
+ update();
1964
+ const sub = service.subscribe(update);
1965
+ return () => sub.unsubscribe();
1966
+ }, [currentInstance]);
1967
+ const destroy = useCallback(async (modelProperty) => {
1968
+ if (!modelProperty)
1969
+ return;
1970
+ setCurrentInstance(modelProperty);
1971
+ await modelProperty.destroy();
1972
+ }, []);
1973
+ const resetError = useCallback(() => {
1974
+ if (currentInstance) {
1975
+ currentInstance.getService().send({ type: 'clearDestroyError' });
1976
+ }
1977
+ }, [currentInstance]);
1978
+ return {
1979
+ destroy,
1980
+ isLoading: destroyState.isLoading,
1981
+ error: destroyState.error,
1982
+ resetError,
1983
+ };
2174
1984
  };
2175
1985
 
2176
1986
  const useDeleteItem = () => {
2177
- const [isDeletingItem, setIsDeletingItem] = useState(false);
1987
+ const [currentInstance, setCurrentInstance] = useState(null);
1988
+ const [destroyState, setDestroyState] = useState({
1989
+ isLoading: false,
1990
+ error: null,
1991
+ });
1992
+ useEffect(() => {
1993
+ if (!currentInstance) {
1994
+ setDestroyState({ isLoading: false, error: null });
1995
+ return;
1996
+ }
1997
+ const service = currentInstance.getService();
1998
+ const update = () => {
1999
+ const snap = service.getSnapshot();
2000
+ const ctx = snap.context;
2001
+ setDestroyState({
2002
+ isLoading: !!ctx._destroyInProgress,
2003
+ error: ctx._destroyError ? new Error(ctx._destroyError.message) : null,
2004
+ });
2005
+ };
2006
+ update();
2007
+ const sub = service.subscribe(update);
2008
+ return () => sub.unsubscribe();
2009
+ }, [currentInstance]);
2178
2010
  const destroy = useCallback(async (item) => {
2179
- if (!item) {
2011
+ if (!item)
2180
2012
  return;
2013
+ setCurrentInstance(item);
2014
+ await item.destroy();
2015
+ }, []);
2016
+ const resetError = useCallback(() => {
2017
+ if (currentInstance) {
2018
+ currentInstance.getService().send({ type: 'clearDestroyError' });
2181
2019
  }
2182
- setIsDeletingItem(true);
2183
- await deleteItem({ seedLocalId: item.seedLocalId });
2184
- setIsDeletingItem(false);
2185
- }, [isDeletingItem]);
2186
- useEffect(() => { }, []);
2020
+ }, [currentInstance]);
2187
2021
  return {
2188
2022
  deleteItem: destroy,
2189
- isDeletingItem,
2023
+ isLoading: destroyState.isLoading,
2024
+ error: destroyState.error,
2025
+ resetError,
2190
2026
  };
2191
2027
  };
2192
2028
 
2029
+ const SEED_QUERY_DEFAULT_OPTIONS = {
2030
+ queries: {
2031
+ networkMode: 'offlineFirst',
2032
+ gcTime: 1000 * 60 * 60 * 24, // 24 hours
2033
+ staleTime: 1000 * 60, // 1 minute - list data can be slightly stale
2034
+ },
2035
+ };
2036
+ /**
2037
+ * Returns the default options used by Seed for list-query caching.
2038
+ * Use this when building your own QueryClient so Seed hooks get consistent behavior.
2039
+ */
2040
+ function getSeedQueryDefaultOptions() {
2041
+ return { ...SEED_QUERY_DEFAULT_OPTIONS };
2042
+ }
2043
+ /**
2044
+ * Merges Seed's default query options with your existing default options.
2045
+ * Your options take precedence over Seed's. Use when constructing your own QueryClient:
2046
+ *
2047
+ * @example
2048
+ * ```ts
2049
+ * const client = new QueryClient({
2050
+ * defaultOptions: mergeSeedQueryDefaults({
2051
+ * queries: { gcTime: 1000 * 60 * 60 },
2052
+ * }),
2053
+ * })
2054
+ * ```
2055
+ */
2056
+ function mergeSeedQueryDefaults(userOptions) {
2057
+ const seed = getSeedQueryDefaultOptions();
2058
+ if (!userOptions)
2059
+ return seed;
2060
+ return {
2061
+ queries: {
2062
+ ...seed.queries,
2063
+ ...(userOptions.queries ?? {}),
2064
+ },
2065
+ mutations: {
2066
+ ...(seed.mutations ?? {}),
2067
+ ...(userOptions.mutations ?? {}),
2068
+ },
2069
+ };
2070
+ }
2071
+ /**
2072
+ * Creates a QueryClient configured with Seed's default options.
2073
+ * Use this when you want to provide your own QueryClientProvider but still use Seed's defaults.
2074
+ *
2075
+ * @param overrides - Optional config to merge with Seed defaults (e.g. defaultOptions, logger).
2076
+ */
2077
+ function createSeedQueryClient(overrides) {
2078
+ const defaults = getSeedQueryDefaultOptions();
2079
+ const { defaultOptions: userDefaultOptions, ...restOverrides } = overrides ?? {};
2080
+ return new QueryClient$1({
2081
+ ...restOverrides,
2082
+ defaultOptions: userDefaultOptions
2083
+ ? mergeSeedQueryDefaults(userDefaultOptions)
2084
+ : defaults,
2085
+ });
2086
+ }
2087
+
2088
+ /** Module-level ref so invalidateItemPropertiesForItem works when test and app share the same bundle but not the same window (e.g. iframe). */
2089
+ let invalidateItemPropertiesRef = null;
2090
+ /**
2091
+ * Invalidates and refetches the item-properties query for an item.
2092
+ * Call this after updating an ItemProperty (e.g. after save()) so useItemProperties
2093
+ * refetches and the UI updates. Returns a Promise that resolves when the refetch has completed (if available).
2094
+ */
2095
+ function invalidateItemPropertiesForItem(canonicalId) {
2096
+ const p1 = invalidateItemPropertiesRef?.(canonicalId);
2097
+ if (typeof window !== 'undefined' && window.__SEED_INVALIDATE_ITEM_PROPERTIES__) {
2098
+ window.__SEED_INVALIDATE_ITEM_PROPERTIES__(canonicalId);
2099
+ }
2100
+ return Promise.resolve(p1).then(() => { });
2101
+ }
2102
+ function SeedProviderEventSubscriber({ queryClient }) {
2103
+ useEffect(() => {
2104
+ const invalidate = (canonicalId) => {
2105
+ const key = ['seed', 'itemProperties', canonicalId];
2106
+ queryClient.invalidateQueries({ queryKey: key });
2107
+ return queryClient.refetchQueries({ queryKey: key });
2108
+ };
2109
+ invalidateItemPropertiesRef = invalidate;
2110
+ if (typeof window !== 'undefined') {
2111
+ window.__SEED_INVALIDATE_ITEM_PROPERTIES__ = invalidate;
2112
+ }
2113
+ const handler = (payload) => {
2114
+ const canonicalId = payload?.seedLocalId ?? payload?.seedUid;
2115
+ if (canonicalId) {
2116
+ invalidate(canonicalId);
2117
+ }
2118
+ };
2119
+ eventEmitter.on('itemProperty.saved', handler);
2120
+ return () => {
2121
+ eventEmitter.off('itemProperty.saved', handler);
2122
+ invalidateItemPropertiesRef = null;
2123
+ if (typeof window !== 'undefined') {
2124
+ window.__SEED_INVALIDATE_ITEM_PROPERTIES__ = null;
2125
+ }
2126
+ };
2127
+ }, [queryClient]);
2128
+ return null;
2129
+ }
2130
+ /**
2131
+ * Provider that supplies a React Query client to Seed list hooks (useSchemas, useItems, useModels, etc.)
2132
+ * so results are cached and shared across components. Wrap your app (or the subtree that uses Seed hooks)
2133
+ * after calling client.init().
2134
+ *
2135
+ * - No props: uses an internal QueryClient with Seed defaults.
2136
+ * - queryClient prop: use your own client (e.g. merge getSeedQueryDefaultOptions when creating it).
2137
+ */
2138
+ function SeedProvider({ children, queryClient: queryClientProp, queryClientRef }) {
2139
+ const queryClient = useMemo(() => queryClientProp ?? createSeedQueryClient(), [queryClientProp]);
2140
+ if (queryClientRef) {
2141
+ queryClientRef.current = queryClient;
2142
+ if (typeof window !== 'undefined') {
2143
+ const w = window;
2144
+ w.__TEST_SEED_QUERY_CLIENT__ = queryClient;
2145
+ try {
2146
+ if (window.parent && window.parent !== window)
2147
+ window.parent.__TEST_SEED_QUERY_CLIENT__ = queryClient;
2148
+ }
2149
+ catch {
2150
+ // cross-origin frame, ignore
2151
+ }
2152
+ }
2153
+ }
2154
+ return (React.createElement(QueryClientProvider, { client: queryClient },
2155
+ React.createElement(SeedProviderEventSubscriber, { queryClient: queryClient }),
2156
+ children));
2157
+ }
2158
+
2193
2159
  debug('seedSdk:feed');
2194
2160
  const relationValuesToExclude = [
2195
2161
  '0x0000000000000000000000000000000000000000000000000000000000000020',
@@ -2715,5 +2681,5 @@ const getFeedItemsBySchemaName = async (schemaName) => {
2715
2681
  return feedItems;
2716
2682
  };
2717
2683
 
2718
- export { BaseEasClient as EasClient, Item, ItemProperty, Model, ModelProperty, Schema, getFeedItemsBySchemaName, getItemPropertiesFromEas, getItemVersionsFromEas, getPropertySchema, getSeedsBySchemaName, useAllSchemaVersions, useCreateItem, useCreateSchema, useDeleteItem, useItem, useItemProperties, useItemProperty, useItems, useModel, useModelProperties, useModelProperty, useModels, useSchema, useSchemas };
2684
+ export { BaseEasClient as EasClient, Item, ItemProperty, Model, ModelProperty, Schema, SeedProvider, createSeedQueryClient, getFeedItemsBySchemaName, getItemPropertiesFromEas, getItemVersionsFromEas, getPropertySchema, getSeedQueryDefaultOptions, getSeedsBySchemaName, invalidateItemPropertiesForItem, mergeSeedQueryDefaults, useAllSchemaVersions, useCreateItem, useCreateItemProperty, useCreateModel, useCreateModelProperty, useCreateSchema, useDeleteItem, useDestroyItemProperty, useDestroyModel, useDestroyModelProperty, useDestroySchema, useItem, useItemProperties, useItemProperty, useItems, useModel, useModelProperties, useModelProperty, useModels, usePublishItem, useSchema, useSchemas };
2719
2685
  //# sourceMappingURL=main.js.map