Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/funny-falcons-cover.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@itwin/presentation-core-interop": minor
---

Added a new `createECSchemaProvider` overload accepting a `SchemaView` instance (from `@itwin/ecschema-metadata`) that naturally provides synchronous access.
14 changes: 14 additions & 0 deletions .changeset/sync-ec-types-schema-view.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
"@itwin/presentation-shared": major
"@itwin/presentation-core-interop": major
"@itwin/presentation-hierarchies": major
---

`EC` namespace interfaces in `@itwin/presentation-shared` no longer use `Promise` wrappers — once a schema is loaded via the still-async `ECSchemaProvider.getSchema`, all further navigation (`baseClass`, `is()`, `getProperty()`, `getProperties()`, `getDerivedClasses()`, `kindOfQuantity`, `relationshipClass`, `enumeration`, `abstractConstraint`) is synchronous.

Additional changes:

- `createECSchemaProvider` in `@itwin/presentation-core-interop` now force-loads all schema items when constructing an `EC.Schema` from `SchemaContext`, so the synchronous contract can be met.
- The `getCustomAttributes()` method has been removed from `EC.Schema`, `EC.Class`, and `EC.Property` and replaced with an `isHidden: boolean` property. `EC.CustomAttributeSet` and `EC.CustomAttribute` types have been removed.
- Added an optional `EC.Property.category` attribute.
- Added a required `EC.RelationshipConstraint.constraintClasses` attribute.
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ describe("hide if no children", () => {
setup,
cleanup,
test: async (iModel) => {
const provider = new StatelessHierarchyProvider({
const provider = await StatelessHierarchyProvider.create({
iModel,
getHierarchyFactory: (imodelAccess) => {
return createPredicateBasedHierarchyDefinition({
Expand Down Expand Up @@ -65,7 +65,7 @@ describe("hide if no children", () => {
setup,
cleanup,
test: async (iModel) => {
const provider = new StatelessHierarchyProvider({
const provider = await StatelessHierarchyProvider.create({
iModel,
getHierarchyFactory: (imodelAccess) => {
return createPredicateBasedHierarchyDefinition({
Expand Down
8 changes: 4 additions & 4 deletions apps/performance-tests/src/hierarchies/ModelsTree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ describe("models tree", () => {
setup,
cleanup,
test: async (iModel) => {
const provider = new StatelessHierarchyProvider({ iModel, getHierarchyFactory });
const provider = await StatelessHierarchyProvider.create({ iModel, getHierarchyFactory });
const result = await provider.loadHierarchy({ depth: 2 });
expect(result).toBeGreaterThan(0);
},
Expand All @@ -47,7 +47,7 @@ describe("models tree", () => {
setup,
cleanup,
test: async (iModel) => {
const provider = new StatelessHierarchyProvider({ iModel, getHierarchyFactory });
const provider = await StatelessHierarchyProvider.create({ iModel, getHierarchyFactory });
const result = await provider.loadHierarchy();
expect(result).toBeGreaterThan(0);
},
Expand All @@ -57,7 +57,7 @@ describe("models tree", () => {
testName: "creates initial filtered view for 50k target items",
setup: async () => {
const iModel = SnapshotDb.openFile(Datasets.getIModelPath("50k functional 3D elements"));
const imodelAccess = StatelessHierarchyProvider.createIModelAccess(iModel, "unbounded");
const imodelAccess = await StatelessHierarchyProvider.createIModelAccess(iModel, "unbounded");
const targetItems = new Array<InstanceKey>();
const query: ECSqlQueryDef = {
ecsql: `SELECT CAST(IdToHex(ECInstanceId) AS TEXT) AS ECInstanceId FROM bis.GeometricElement3d`,
Expand All @@ -81,7 +81,7 @@ describe("models tree", () => {
}),
};
expect(search.paths).toHaveLength(50000);
const provider = new StatelessHierarchyProvider({
const provider = await StatelessHierarchyProvider.create({
imodelAccess,
getHierarchyFactory: () => new ModelsTreeDefinition({ imodelAccess, idsCache }),
search: { paths: await HierarchySearchTree.createFromPathsList(search.paths) },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ export function runHierarchyTest(
};
},
test: async (props) => {
const provider = new StatelessHierarchyProvider({ ...props, rowLimit: "unbounded" });
const provider = await StatelessHierarchyProvider.create({ ...props, rowLimit: "unbounded" });
const nodeCount = await provider.loadHierarchy();
if (testProps.expectedNodeCount !== undefined) {
expect(nodeCount).toBe(testProps.expectedNodeCount);
Expand Down
2 changes: 1 addition & 1 deletion apps/performance-tests/src/hierarchies/Search.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ describe("search", () => {
props.iModel.close();
},
test: async ({ search, ...props }) => {
const provider = new StatelessHierarchyProvider({
const provider = await StatelessHierarchyProvider.create({
...props,
search: { paths: await HierarchySearchTree.createFromPathsList(search.paths) },
rowLimit: "unbounded",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,9 @@ import type {
HierarchyNode,
HierarchyProvider,
HierarchySearchTree,
LimitingECSqlQueryExecutor,
} from "@itwin/presentation-hierarchies";
import type {
EC,
ECClassHierarchyInspector,
ECSchemaProvider,
ECSqlQueryDef,
ECSqlQueryExecutor,
ECSqlQueryReaderOptions,
} from "@itwin/presentation-shared";
import type { ECClassHierarchyInspector, ECSchemaProvider } from "@itwin/presentation-shared";

interface ProviderOptionsBase {
rowLimit?: number | "unbounded";
Expand All @@ -46,22 +40,12 @@ function log(messageOrCallback: string | (() => string)) {

const DEFAULT_ROW_LIMIT = 1000;

export interface IModelAccess {
createQueryReader(
query: ECSqlQueryDef,
config?: ECSqlQueryReaderOptions & { limit?: number | "unbounded" },
): ReturnType<ECSqlQueryExecutor["createQueryReader"]>;
classDerivesFrom(derivedClassFullName: string, candidateBaseClassFullName: string): Promise<boolean> | boolean;
getSchema(schemaName: string): Promise<EC.Schema | undefined>;
imodelKey: string;
}
export type IModelAccess = ECSchemaProvider &
ECClassHierarchyInspector &
LimitingECSqlQueryExecutor & { imodelKey: string };

export class StatelessHierarchyProvider {
private readonly _provider: HierarchyProvider;

constructor(private readonly _props: ProviderOptions) {
this._provider = this.createProvider();
}
private constructor(private readonly _provider: HierarchyProvider) {}

public async loadHierarchy(props?: { depth?: number }): Promise<number> {
const depth = props?.depth;
Expand All @@ -86,20 +70,21 @@ export class StatelessHierarchyProvider {
});
}

private createProvider() {
public static async create(props: ProviderOptions): Promise<StatelessHierarchyProvider> {
const imodelAccess =
"iModel" in this._props
? StatelessHierarchyProvider.createIModelAccess(this._props.iModel, this._props.rowLimit)
: this._props.imodelAccess;
return createIModelHierarchyProvider({
"iModel" in props
? await StatelessHierarchyProvider.createIModelAccess(props.iModel, props.rowLimit)
: props.imodelAccess;
const hierarchyProvider = createIModelHierarchyProvider({
imodelAccess,
hierarchyDefinition: this._props.getHierarchyFactory(imodelAccess),
hierarchyDefinition: props.getHierarchyFactory(imodelAccess),
queryCacheSize: 0,
search: this._props.search ? { paths: this._props.search.paths } : undefined,
search: props.search ? { paths: props.search.paths } : undefined,
});
return new StatelessHierarchyProvider(hierarchyProvider);
}

public static createIModelAccess(iModel: IModelDb, rowLimit?: number | "unbounded"): IModelAccess {
public static async createIModelAccess(iModel: IModelDb, rowLimit?: number | "unbounded"): Promise<IModelAccess> {
const schemaProvider = createECSchemaProvider(iModel.schemaContext);
const rowLimitToUse = rowLimit ?? DEFAULT_ROW_LIMIT;
const imodelAccess = {
Expand Down
1 change: 1 addition & 0 deletions apps/performance-tests/src/setup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ beforeAll(async () => {
Logger.setLevelDefault(LogLevel.Warning);
Logger.setLevel("i18n", LogLevel.Error);
Logger.setLevel("SQLite", LogLevel.Error);
Logger.setLevel("ECDb", LogLevel.None);

await IModelHost.startup({ profileName: "presentation-performance-tests" });
await Datasets.initialize("./datasets");
Expand Down
7 changes: 7 additions & 0 deletions packages/core-interop/api/presentation-core-interop.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import type { QueryRowProxy } from '@itwin/core-common';
import type { Schema } from '@itwin/ecschema-metadata';
import type { SchemaContext } from '@itwin/ecschema-metadata';
import { SchemaKey } from '@itwin/ecschema-metadata';
import type { SchemaView } from '@itwin/ecschema-metadata';
import type { UnitSystemKey } from '@itwin/core-quantity';

// @public
Expand All @@ -41,6 +42,9 @@ interface CoreSchemaContext {
// @public
export function createECSchemaProvider(schemaContext: CoreSchemaContext): ECSchemaProvider;

// @beta
export function createECSchemaProvider(schemaView: PublicSchemaView): ECSchemaProvider;

// @public
export function createECSqlQueryExecutor(imodel: CoreECSqlReaderFactory): ECSqlQueryExecutor;

Expand Down Expand Up @@ -81,6 +85,9 @@ interface ICoreTxnManager {
onCommitted: Event_2;
}

// @public
type PublicSchemaView = Pick<SchemaView, "schemaToken" | "isOutdated" | "schemaCount" | "classCount" | "getSchema" | "getSchemaByAlias" | "getSchemas" | "findClass" | "findEnumeration" | "findKindOfQuantity" | "findPropertyCategory">;

// @public
export function registerTxnListeners(txns: ICoreTxnManager, onChanged: () => void): () => void;

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
sep=;
Release Tag;API Item Type;API Item Name
public;function;createECSchemaProvider
beta;function;createECSchemaProvider
public;function;createECSqlQueryExecutor
public;function;createIModelKey
public;function;createLogger
Expand Down
10 changes: 5 additions & 5 deletions packages/core-interop/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,11 @@
"rxjs": "catalog:rxjs"
},
"peerDependencies": {
"@itwin/core-bentley": "^5.0.0",
"@itwin/core-common": "^5.0.0",
"@itwin/core-geometry": "^5.0.0",
"@itwin/core-quantity": "^5.0.0",
"@itwin/ecschema-metadata": "^5.0.0"
"@itwin/core-bentley": "^5.10.1",
"@itwin/core-common": "^5.10.1",
"@itwin/core-geometry": "^5.10.1",
"@itwin/core-quantity": "^5.10.1",
"@itwin/ecschema-metadata": "^5.10.1"
},
"devDependencies": {
"@itwin/build-tools": "catalog:build-tools",
Expand Down
77 changes: 32 additions & 45 deletions packages/core-interop/src/core-interop/Metadata.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,23 +3,33 @@
* See LICENSE.md in the project root for license terms and full copyright notice.
*--------------------------------------------------------------------------------------------*/

import { SchemaKey as CoreSchemaKey } from "@itwin/ecschema-metadata";
import { createECSchema } from "./MetadataInternal.js";
import { createECSchemaProviderFromSchemaContext } from "./schema-provider/SchemaContextProvider.js";
import { createECSchemaProviderFromSchemaView } from "./schema-provider/SchemaViewProvider.js";

import type { Schema as CoreSchema } from "@itwin/ecschema-metadata";
import type { EC, ECSchemaProvider } from "@itwin/presentation-shared";
import type { ECSchemaProvider } from "@itwin/presentation-shared";
import type { CoreSchemaContext } from "./schema-provider/SchemaContextProvider.js";
import type { PublicSchemaView } from "./schema-provider/SchemaViewProvider.js";

/**
* Defines input for `createECSchemaProvider`. Generally, this is an instance of [SchemaContext](https://www.itwinjs.org/reference/ecschema-metadata/context/schemacontext/)
* class from `@itwin/ecschema-metadata` package.
* Creates an `ECSchemaProvider` for given [SchemaContext](https://www.itwinjs.org/reference/ecschema-metadata/context/schemacontext/).
*
* Usage example:
*
* ```ts
* import { IModelConnection } from "@itwin/core-frontend";
* import { createECSchemaProvider } from "@itwin/presentation-core-interop";
*
* const imodel: IModelConnection = getIModel();
* const schemaProvider = createECSchemaProvider(imodel.schemaContext);
* // the created schema provider may be used in `@itwin/presentation-hierarchies` and other Presentation packages
* ```
*
* @public
*/
interface CoreSchemaContext {
getSchema(key: CoreSchemaKey): Promise<CoreSchema | undefined>;
}
export function createECSchemaProvider(schemaContext: CoreSchemaContext): ECSchemaProvider;

/**
* Creates an `ECSchemaProvider` for given [SchemaContext](https://www.itwinjs.org/reference/ecschema-metadata/context/schemacontext/).
* Creates an `ECSchemaProvider` for a given [SchemaView](https://www.itwinjs.org/reference/ecschema-metadata/context/schemaview/).
*
* Usage example:
*
Expand All @@ -28,44 +38,21 @@ interface CoreSchemaContext {
* import { createECSchemaProvider } from "@itwin/presentation-core-interop";
*
* const imodel: IModelConnection = getIModel();
* const schemaProvider = createECSchemaProvider(imodel.schemaContext);
* const schemaProvider = createECSchemaProvider(await imodel.getSchemaView());
* // the created schema provider may be used in `@itwin/presentation-hierarchies` and other Presentation packages
* ```
*
* @public
* @beta
*/
export function createECSchemaProvider(schemaContext: CoreSchemaContext): ECSchemaProvider {
const schemaRequestsCache = new Map<string, Promise<EC.Schema | undefined>>();
async function getSchemaUnprotected(schemaName: string) {
const coreSchema = await schemaContext.getSchema(new CoreSchemaKey(schemaName));
return coreSchema ? createECSchema(coreSchema) : undefined;
}
async function getSchemaProtected(schemaName: string, handledExistingSchemaErrors: Set<string>) {
// workaround for https://github.com/iTwin/itwinjs-core/issues/6542
try {
return await getSchemaUnprotected(schemaName);
} catch (e) {
/* v8 ignore else -- @preserve */
if (e instanceof Error) {
if (e.message.includes("already exists within this cache") && !handledExistingSchemaErrors.has(schemaName)) {
handledExistingSchemaErrors.add(schemaName);
return getSchemaProtected(schemaName, handledExistingSchemaErrors);
}
if (e.message.includes("schema not found")) {
return undefined;
}
}
throw e;
}
export function createECSchemaProvider(schemaView: PublicSchemaView): ECSchemaProvider;

export function createECSchemaProvider(input: CoreSchemaContext | PublicSchemaView): ECSchemaProvider {
if (isSchemaView(input)) {
return createECSchemaProviderFromSchemaView(input);
}
return {
async getSchema(name) {
let promise = schemaRequestsCache.get(name);
if (!promise) {
promise = getSchemaProtected(name, new Set());
schemaRequestsCache.set(name, promise);
}
return promise;
},
};
return createECSchemaProviderFromSchemaContext(input);
}

function isSchemaView(input: CoreSchemaContext | PublicSchemaView): input is PublicSchemaView {
return "schemaToken" in input;
}
Loading
Loading