@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.
- package/README.md +38 -348
- package/dist/{ArweaveClient-CleX_4Gw.js → ArweaveClient-CgWK-JgT.js} +8 -8
- package/dist/{ArweaveClient-CleX_4Gw.js.map → ArweaveClient-CgWK-JgT.js.map} +1 -1
- package/dist/{ArweaveClient-BvJ1FhQ5.js → ArweaveClient-WcG8CZAE.js} +8 -8
- package/dist/{ArweaveClient-BvJ1FhQ5.js.map → ArweaveClient-WcG8CZAE.js.map} +1 -1
- package/dist/{Db-DX08SxS9.js → Db-DjFdIdR9.js} +9 -16
- package/dist/{Db-DX08SxS9.js.map → Db-DjFdIdR9.js.map} +1 -1
- package/dist/{Db-BPnO1-_p.js → Db-DjofXdeU.js} +9 -9
- package/dist/{Db-BPnO1-_p.js.map → Db-DjofXdeU.js.map} +1 -1
- package/dist/{EasClient-BwhUcPjY.js → EasClient-Aojewp6P.js} +8 -8
- package/dist/{EasClient-CJSs38Db.js.map → EasClient-Aojewp6P.js.map} +1 -1
- package/dist/{EasClient-CJSs38Db.js → EasClient-BVFXp2O6.js} +8 -8
- package/dist/{EasClient-BwhUcPjY.js.map → EasClient-BVFXp2O6.js.map} +1 -1
- package/dist/{FileManager-B1tdLMsX.js → FileManager-C9zr4AJe.js} +8 -8
- package/dist/{FileManager-B1tdLMsX.js.map → FileManager-C9zr4AJe.js.map} +1 -1
- package/dist/{FileManager-Ct91ZhOE.js → FileManager-CxGJLw5C.js} +8 -8
- package/dist/{FileManager-Ct91ZhOE.js.map → FileManager-CxGJLw5C.js.map} +1 -1
- package/dist/Item/Item.d.ts +28 -7
- package/dist/Item/Item.d.ts.map +1 -1
- package/dist/Item/service/actors/runPublish.d.ts +5 -0
- package/dist/Item/service/actors/runPublish.d.ts.map +1 -0
- package/dist/Item/service/itemMachineSingle.d.ts +10 -5
- package/dist/Item/service/itemMachineSingle.d.ts.map +1 -1
- package/dist/ItemProperty/ItemProperty.d.ts +30 -5
- package/dist/ItemProperty/ItemProperty.d.ts.map +1 -1
- package/dist/ItemProperty/service/actors/loadOrCreateProperty.d.ts.map +1 -1
- package/dist/ItemProperty/service/propertyMachine.d.ts +10 -10
- package/dist/ItemProperty/service/propertyMachine.d.ts.map +1 -1
- package/dist/Model/Model.d.ts +27 -20
- package/dist/Model/Model.d.ts.map +1 -1
- package/dist/Model/index.d.ts +1 -1
- package/dist/Model/service/actors/createModelProperties.d.ts.map +1 -1
- package/dist/Model/service/actors/loadOrCreateModel.d.ts.map +1 -1
- package/dist/Model/service/actors/validateModel.d.ts.map +1 -1
- package/dist/Model/service/modelMachine.d.ts +18 -3
- package/dist/Model/service/modelMachine.d.ts.map +1 -1
- package/dist/ModelProperty/ModelProperty.d.ts +25 -2
- package/dist/ModelProperty/ModelProperty.d.ts.map +1 -1
- package/dist/ModelProperty/service/actors/compareAndMarkDraft.d.ts.map +1 -1
- package/dist/ModelProperty/service/actors/saveToSchema.d.ts.map +1 -1
- package/dist/ModelProperty/service/actors/validateProperty.d.ts.map +1 -1
- package/dist/ModelProperty/service/modelPropertyMachine.d.ts +17 -3
- package/dist/ModelProperty/service/modelPropertyMachine.d.ts.map +1 -1
- package/dist/{ModelProperty-Cr3BmgkC.js → ModelProperty-CGdkocQ8.js} +349 -817
- package/dist/ModelProperty-CGdkocQ8.js.map +1 -0
- package/dist/{PathResolver-DJdxE_OK.js → PathResolver-CX6GHoTS.js} +8 -8
- package/dist/{PathResolver-DJdxE_OK.js.map → PathResolver-CX6GHoTS.js.map} +1 -1
- package/dist/{PathResolver-BErmcZqP.js → PathResolver-z_WX47_o.js} +8 -8
- package/dist/{PathResolver-BErmcZqP.js.map → PathResolver-z_WX47_o.js.map} +1 -1
- package/dist/{QueryClient-DIu9c-w6.js → QueryClient-ByKPdRmE.js} +8 -8
- package/dist/{QueryClient-DIu9c-w6.js.map → QueryClient-ByKPdRmE.js.map} +1 -1
- package/dist/{QueryClient-D2mv63gP.js → QueryClient-Cb1iJO-x.js} +8 -8
- package/dist/{QueryClient-D2mv63gP.js.map → QueryClient-Cb1iJO-x.js.map} +1 -1
- package/dist/Schema/Schema.d.ts +24 -3
- package/dist/Schema/Schema.d.ts.map +1 -1
- package/dist/Schema/service/actors/checkExistingSchema.d.ts.map +1 -1
- package/dist/Schema/service/actors/createPropertyInstances.d.ts.map +1 -1
- package/dist/Schema/service/actors/loadOrCreateSchema.d.ts.map +1 -1
- package/dist/Schema/service/actors/verifyPropertyInstancesInCache.d.ts.map +1 -1
- package/dist/Schema/service/actors/writeModelsToDb.d.ts.map +1 -1
- package/dist/Schema/service/actors/writePropertiesToDb.d.ts.map +1 -1
- package/dist/Schema/service/actors/writeSchemaToDb.d.ts.map +1 -1
- package/dist/Schema/service/addModelsMachine.d.ts.map +1 -1
- package/dist/Schema/service/schemaMachine.d.ts +17 -3
- package/dist/Schema/service/schemaMachine.d.ts.map +1 -1
- package/dist/{Schema-DeKabJ0T.js → Schema-D1eqDHyt.js} +995 -186
- package/dist/Schema-D1eqDHyt.js.map +1 -0
- package/dist/{SchemaValidationService-cTlURuDt.js → SchemaValidationService-DyttFaV_.js} +7 -7
- package/dist/{SchemaValidationService-cTlURuDt.js.map → SchemaValidationService-DyttFaV_.js.map} +1 -1
- package/dist/browser/db/Db.d.ts.map +1 -1
- package/dist/browser/react/SeedProvider.d.ts +30 -0
- package/dist/browser/react/SeedProvider.d.ts.map +1 -0
- package/dist/browser/react/index.d.ts +4 -1
- package/dist/browser/react/index.d.ts.map +1 -1
- package/dist/browser/react/item.d.ts +10 -6
- package/dist/browser/react/item.d.ts.map +1 -1
- package/dist/browser/react/itemProperty.d.ts +37 -1
- package/dist/browser/react/itemProperty.d.ts.map +1 -1
- package/dist/browser/react/liveQuery.d.ts.map +1 -1
- package/dist/browser/react/model.d.ts +21 -7
- package/dist/browser/react/model.d.ts.map +1 -1
- package/dist/browser/react/modelProperty.d.ts +23 -0
- package/dist/browser/react/modelProperty.d.ts.map +1 -1
- package/dist/browser/react/queryClient.d.ts +28 -0
- package/dist/browser/react/queryClient.d.ts.map +1 -0
- package/dist/browser/react/schema.d.ts +8 -0
- package/dist/browser/react/schema.d.ts.map +1 -1
- package/dist/browser/react/trash.d.ts +5 -2
- package/dist/browser/react/trash.d.ts.map +1 -1
- package/dist/cjs/{ModelProperty-MkN5Rmx7.js → ModelProperty-BeJvgKMw.js} +377 -477
- package/dist/cjs/ModelProperty-BeJvgKMw.js.map +1 -0
- package/dist/cjs/{Schema-B5cr_JVK.js → Schema-CVs9J6eP.js} +709 -263
- package/dist/cjs/Schema-CVs9J6eP.js.map +1 -0
- package/dist/cjs/{SchemaValidationService-BgIzc3-r.js → SchemaValidationService-CDKcVRFQ.js} +4 -4
- package/dist/cjs/{SchemaValidationService-BgIzc3-r.js.map → SchemaValidationService-CDKcVRFQ.js.map} +1 -1
- package/dist/cjs/{getItem-CVJJPky2.js → getItem-B5RYPvrG.js} +4 -4
- package/dist/cjs/{getItem-CVJJPky2.js.map → getItem-B5RYPvrG.js.map} +1 -1
- package/dist/cjs/{getPublishPayload-DbOc3WA-.js → getPublishPayload-BD1qRob1.js} +26 -11
- package/dist/cjs/getPublishPayload-BD1qRob1.js.map +1 -0
- package/dist/cjs/{getPublishUploads-NzioLz-3.js → getPublishUploads-CnC9aYxs.js} +5 -5
- package/dist/cjs/getPublishUploads-CnC9aYxs.js.map +1 -0
- package/dist/cjs/{getSegmentedItemProperties-BsaklLwI.js → getSegmentedItemProperties-B_njnntx.js} +2 -2
- package/dist/cjs/{getSegmentedItemProperties-BsaklLwI.js.map → getSegmentedItemProperties-B_njnntx.js.map} +1 -1
- package/dist/cjs/{index-BmIVfqGN.js → index-BeKPbbk0.js} +12715 -12384
- package/dist/cjs/index-BeKPbbk0.js.map +1 -0
- package/dist/cjs/{index-C_0angRB.js → index-Dnywap_P.js} +4 -4
- package/dist/cjs/index-Dnywap_P.js.map +1 -0
- package/dist/client/actors/platformClassesInit.d.ts.map +1 -1
- package/dist/client/actors/processSchemaFiles.d.ts.map +1 -1
- package/dist/client/actors/saveAppState.d.ts.map +1 -1
- package/dist/db/read/getItemData.d.ts.map +1 -1
- package/dist/db/read/getItems.d.ts.map +1 -1
- package/dist/db/read/getModelPropertiesData.d.ts +19 -0
- package/dist/db/read/getModelPropertiesData.d.ts.map +1 -0
- package/dist/db/read/getModelsData.d.ts +15 -0
- package/dist/db/read/getModelsData.d.ts.map +1 -0
- package/dist/db/read/getPublishPayload.d.ts.map +1 -1
- package/dist/db/read/getPublishUploads.d.ts +1 -7
- package/dist/db/read/getPublishUploads.d.ts.map +1 -1
- package/dist/db/read/getSchemaUidForModel.d.ts.map +1 -1
- package/dist/db/write/updateSeedUid.d.ts +7 -0
- package/dist/db/write/updateSeedUid.d.ts.map +1 -0
- package/dist/eas.d.ts.map +1 -1
- package/dist/events/item/index.d.ts.map +1 -1
- package/dist/events/item/syncDbWithEas.d.ts.map +1 -1
- package/dist/{getItem-CcttmUY_.js → getItem-BB5HBCbK.js} +8 -8
- package/dist/{getItem-CcttmUY_.js.map → getItem-BB5HBCbK.js.map} +1 -1
- package/dist/{getPublishPayload-NFpqbd_H.js → getPublishPayload-uLm0AqN_.js} +29 -14
- package/dist/getPublishPayload-uLm0AqN_.js.map +1 -0
- package/dist/{getPublishUploads-Cpb9vgwE.js → getPublishUploads-Dc-HqhO8.js} +9 -9
- package/dist/getPublishUploads-Dc-HqhO8.js.map +1 -0
- package/dist/{getSegmentedItemProperties-DiyQPMgI.js → getSegmentedItemProperties-BrIqFNfD.js} +2 -2
- package/dist/{getSegmentedItemProperties-DiyQPMgI.js.map → getSegmentedItemProperties-BrIqFNfD.js.map} +1 -1
- package/dist/helpers/db.d.ts +12 -0
- package/dist/helpers/db.d.ts.map +1 -1
- package/dist/helpers/entity/entityDestroy.d.ts +41 -0
- package/dist/helpers/entity/entityDestroy.d.ts.map +1 -0
- package/dist/helpers/entity/index.d.ts +1 -0
- package/dist/helpers/entity/index.d.ts.map +1 -1
- package/dist/helpers/index.d.ts +1 -0
- package/dist/helpers/index.d.ts.map +1 -1
- package/dist/helpers/property/index.d.ts +12 -12
- package/dist/helpers/property/index.d.ts.map +1 -1
- package/dist/helpers/reactiveProxy.d.ts.map +1 -1
- package/dist/helpers/schema.d.ts.map +1 -1
- package/dist/helpers/updateSchema.d.ts +9 -0
- package/dist/helpers/updateSchema.d.ts.map +1 -1
- package/dist/helpers/waitForEntityIdle.d.ts +2 -2
- package/dist/helpers/waitForEntityIdle.d.ts.map +1 -1
- package/dist/imports/json.d.ts.map +1 -1
- package/dist/{index-r45w9hEq.js → index-2FcQHgKp.js} +2 -2
- package/dist/index-2FcQHgKp.js.map +1 -0
- package/dist/{json-I3vJhXo8.js → index-DPll6EAp.js} +12450 -12121
- package/dist/index-DPll6EAp.js.map +1 -0
- package/dist/{index-CRuq6HVi.js → index-LEY0Og1p.js} +9 -9
- package/dist/index-LEY0Og1p.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/interfaces/IItem.d.ts +2 -0
- package/dist/interfaces/IItem.d.ts.map +1 -1
- package/dist/interfaces/IItemProperty.d.ts +1 -0
- package/dist/interfaces/IItemProperty.d.ts.map +1 -1
- package/dist/main.cjs +3 -3
- package/dist/main.js +999 -1033
- package/dist/main.js.map +1 -1
- package/dist/node.js +16 -16
- package/dist/node.js.map +1 -1
- package/dist/{property-Dy09KTxg.js → property-B15X7jLX.js} +7 -5
- package/dist/property-B15X7jLX.js.map +1 -0
- package/dist/{queries-LZYSuhtz.js → queries-BPDSpiEX.js} +2 -2
- package/dist/{queries-LZYSuhtz.js.map → queries-BPDSpiEX.js.map} +1 -1
- package/dist/services/write/actors/validateEntity.d.ts.map +1 -1
- package/dist/services/write/actors/writeToDatabase.d.ts.map +1 -1
- package/dist/services/write/writeProcessMachine.d.ts +1 -1
- package/dist/types/index.d.ts +9 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/item.d.ts +12 -0
- package/dist/types/item.d.ts.map +1 -1
- package/dist/types/property.d.ts +6 -0
- package/dist/types/property.d.ts.map +1 -1
- package/dist/types/publish.d.ts +9 -0
- package/dist/types/publish.d.ts.map +1 -0
- package/package.json +12 -4
- package/dist/ModelProperty-Cr3BmgkC.js.map +0 -1
- package/dist/Schema-DeKabJ0T.js.map +0 -1
- package/dist/cjs/ModelProperty-MkN5Rmx7.js.map +0 -1
- package/dist/cjs/Schema-B5cr_JVK.js.map +0 -1
- package/dist/cjs/getPublishPayload-DbOc3WA-.js.map +0 -1
- package/dist/cjs/getPublishUploads-NzioLz-3.js.map +0 -1
- package/dist/cjs/index-BmIVfqGN.js.map +0 -1
- package/dist/cjs/index-C_0angRB.js.map +0 -1
- package/dist/events/item/publish.d.ts +0 -7
- package/dist/events/item/publish.d.ts.map +0 -1
- package/dist/getPublishPayload-NFpqbd_H.js.map +0 -1
- package/dist/getPublishUploads-Cpb9vgwE.js.map +0 -1
- package/dist/index-CRuq6HVi.js.map +0 -1
- package/dist/index-r45w9hEq.js.map +0 -1
- package/dist/json-I3vJhXo8.js.map +0 -1
- package/dist/property-Dy09KTxg.js.map +0 -1
package/dist/main.js
CHANGED
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import 'reflect-metadata';
|
|
2
|
-
import {
|
|
3
|
-
export {
|
|
4
|
-
import { g as getPropertySchema } from './property-
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
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 '
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
//
|
|
215
|
-
//
|
|
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
|
|
250
|
-
const loadingItemsRef = useRef(new Set());
|
|
264
|
+
const queryClient = useQueryClient();
|
|
251
265
|
const previousSeedsTableDataRef = useRef(undefined);
|
|
252
|
-
const itemsRef = useRef([]);
|
|
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
|
-
|
|
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
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
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
|
-
|
|
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
|
-
|
|
335
|
+
lastFetchedIdsRef.current = new Set(tableDataItemsSet);
|
|
434
336
|
return;
|
|
435
337
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}, [isClientReady, seedsTableData,
|
|
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 [
|
|
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 (
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
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
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
-
|
|
470
|
-
await Item.find({ modelName, seedLocalId });
|
|
471
|
-
setIsCreatingItem(false);
|
|
472
|
-
}, [isCreatingItem]);
|
|
379
|
+
}, [isLoading]);
|
|
473
380
|
return {
|
|
474
381
|
createItem,
|
|
475
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
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:
|
|
499
|
-
seedUid:
|
|
500
|
-
propertyName
|
|
465
|
+
seedLocalId: resolvedSeedLocalId ?? undefined,
|
|
466
|
+
seedUid: resolvedSeedUid,
|
|
467
|
+
propertyName,
|
|
501
468
|
};
|
|
502
469
|
}
|
|
503
|
-
|
|
504
|
-
|
|
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
|
-
|
|
512
|
-
|
|
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
|
-
|
|
526
|
-
|
|
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
|
-
|
|
544
|
-
|
|
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
|
-
|
|
607
|
-
|
|
608
|
-
|
|
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
|
|
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
|
-
|
|
764
|
-
|
|
765
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
-
|
|
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
|
-
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
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
|
-
|
|
1004
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
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
|
-
|
|
1053
|
-
|
|
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
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
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
|
-
|
|
1105
|
-
|
|
1106
|
-
loadingPropertiesRef.current.clear();
|
|
878
|
+
subscriptionRef.current?.unsubscribe();
|
|
879
|
+
subscriptionRef.current = undefined;
|
|
1107
880
|
};
|
|
1108
881
|
}, []);
|
|
1109
882
|
return {
|
|
1110
|
-
|
|
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
|
|
1206
|
-
const loadingSchemasRef = useRef(new Set());
|
|
1027
|
+
const queryClient = useQueryClient();
|
|
1207
1028
|
const previousSchemasTableDataRef = useRef(undefined);
|
|
1208
|
-
const schemasRef = useRef([]);
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1380
|
-
return;
|
|
1081
|
+
[...currentSchemasSet].every((id) => tableDataSchemasSet.has(id));
|
|
1082
|
+
if (!setsAreEqual) {
|
|
1083
|
+
queryClient.invalidateQueries({ queryKey: SEED_SCHEMAS_QUERY_KEY });
|
|
1381
1084
|
}
|
|
1382
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
}, [
|
|
1118
|
+
}, []);
|
|
1418
1119
|
useEffect(() => {
|
|
1419
|
-
|
|
1420
|
-
|
|
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
|
-
|
|
1507
|
-
|
|
1508
|
-
const
|
|
1509
|
-
const
|
|
1510
|
-
|
|
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
|
|
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
|
-
}
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
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:
|
|
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
|
-
|
|
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;
|
|
1633
|
+
return context._dbId;
|
|
1797
1634
|
}
|
|
1798
1635
|
catch {
|
|
1799
1636
|
return null;
|
|
1800
1637
|
}
|
|
1801
1638
|
}, [model]);
|
|
1802
|
-
|
|
1803
|
-
|
|
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
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
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 (!
|
|
1667
|
+
if (!modelId || modelProperties.length > 0 || !queryClient || !modelPropertiesQueryKey)
|
|
1899
1668
|
return;
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
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 || !
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
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
|
-
|
|
2103
|
-
|
|
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
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
.
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
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 [
|
|
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
|
-
|
|
2183
|
-
await deleteItem({ seedLocalId: item.seedLocalId });
|
|
2184
|
-
setIsDeletingItem(false);
|
|
2185
|
-
}, [isDeletingItem]);
|
|
2186
|
-
useEffect(() => { }, []);
|
|
2020
|
+
}, [currentInstance]);
|
|
2187
2021
|
return {
|
|
2188
2022
|
deleteItem: destroy,
|
|
2189
|
-
|
|
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
|