Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ import kotlinx.coroutines.DelicateCoroutinesApi
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.await
import kotlinx.coroutines.promise
import kotlinx.datetime.toJSDate
import org.modelix.datastructures.model.IGenericModelTree
import org.modelix.model.TreeId
import org.modelix.model.api.INode
import org.modelix.model.api.INodeReference
import org.modelix.model.api.JSNodeConverter
import org.modelix.model.client.IdGenerator
import org.modelix.model.data.ModelData
import org.modelix.model.lazy.CLVersion
import org.modelix.model.lazy.RepositoryId
import org.modelix.model.lazy.createObjectStoreCache
import org.modelix.model.mutable.DummyIdGenerator
import org.modelix.model.mutable.INodeIdGenerator
import org.modelix.model.mutable.ModelixIdGenerator
import org.modelix.model.mutable.VersionedModelTree
import org.modelix.model.mutable.asMutableThreadSafe
import org.modelix.model.mutable.load
import org.modelix.model.mutable.withAutoTransactions
Expand Down Expand Up @@ -80,6 +83,9 @@ sealed class IdSchemeJS() {
object READONLY : IdSchemeJS()
}

@JsExport
data class VersionInformationWithModelTree(val version: VersionInformationJS, val tree: MutableModelTreeJs)

/**
* JS-API for [ModelClientV2].
* Can be used to perform operations on the model server and to read and write model data.
Expand Down Expand Up @@ -111,6 +117,8 @@ interface ClientJS {
*/
fun initRepository(repositoryId: String, useRoleIds: Boolean = true): Promise<Unit>

fun loadReadonlyVersion(repositoryId: String, versionHash: String): Promise<VersionInformationWithModelTree>

/**
* Fetch existing branches for a given repository from the model server.
*
Expand Down Expand Up @@ -192,6 +200,20 @@ internal class ClientJSImpl(private val modelClient: ModelClientV2) : ClientJS {
}
}

override fun loadReadonlyVersion(repositoryId: String, versionHash: String): Promise<VersionInformationWithModelTree> {
return GlobalScope.promise {
val version = modelClient.loadVersion(RepositoryId(repositoryId), versionHash, null)
VersionInformationWithModelTree(
VersionInformationJS(
(version as CLVersion).author,
version.getTimestamp()?.toJSDate(),
version.getContentHash(),
),
MutableModelTreeJsImpl(VersionedModelTree(version).withAutoTransactions()),
)
}
}

override fun dispose() {
modelClient.close()
}
Expand Down
1 change: 1 addition & 0 deletions vue-model-api/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export { useModelsFromJson } from "./useModelsFromJson";
export { useModelClient } from "./useModelClient";
export { useReplicatedModel } from "./useReplicatedModel";
export { useReadonlyVersion } from "./useReadonlyVersion";
137 changes: 137 additions & 0 deletions vue-model-api/src/useReadonlyVersion.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { org } from "@modelix/model-client";
import { toRoleJS } from "@modelix/ts-model-api";
import { watchEffect, type Ref } from "vue";
import { useModelClient } from "./useModelClient";
import { useReadonlyVersion } from "./useReadonlyVersion";
const { loadModelsFromJson } = org.modelix.model.client2;

type VersionInformationWithModelTree =
org.modelix.model.client2.VersionInformationWithModelTree;
type MutableModelTreeJs = org.modelix.model.client2.MutableModelTreeJs;
type ClientJS = org.modelix.model.client2.ClientJS;
type VersionInformationJS = org.modelix.model.client2.VersionInformationJS;

class SuccessfulVersionInformationWithModelTree {
public version: VersionInformationJS;
public tree: MutableModelTreeJs;

constructor(versionHash: string) {
const modelData = {
root: {},
};

const rootNode = loadModelsFromJson([JSON.stringify(modelData)]);
rootNode.setPropertyValue(toRoleJS("versionHash"), versionHash);
this.tree = {
rootNode,
addListener: jest.fn(),
} as unknown as MutableModelTreeJs;
this.version = {
author: "basti",
time: new Date(),
versionHash: "SQnQb*ZS1Vi8Mss2HAu0ENbcpstmqb5E6TuZvpSH1TW4",
} as unknown as VersionInformationJS;
}

addListener = jest.fn();
}

test("test version is loaded", (done) => {
class SuccessfulClientJS {
loadReadonlyVersion(
_repositoryId: string,
_versionHash: string,
): Promise<VersionInformationWithModelTree> {
return Promise.resolve(
new SuccessfulVersionInformationWithModelTree(
_versionHash,
) as unknown as VersionInformationWithModelTree,
);
}

dispose = jest.fn();
}

const { client } = useModelClient("anURL", () =>
Promise.resolve(new SuccessfulClientJS() as unknown as ClientJS),
);
const { rootNode, versionInformation } = useReadonlyVersion(
client,
"aRepository",
"aVersionHash",
);
watchEffect(() => {
if (rootNode.value !== null && versionInformation.value !== null) {
expect(rootNode.value.getPropertyValue(toRoleJS("versionHash"))).toBe(
"aVersionHash",
);
done();
}
});
});

test("test connection error is exposed", (done) => {
class FailingClientJS {
loadReadonlyVersion(
_repositoryId: string,
_versionHash: string,
): Promise<VersionInformationWithModelTree> {
return Promise.reject("Could not connect.");
}
}

const { client } = useModelClient("anURL", () =>
Promise.resolve(new FailingClientJS() as unknown as ClientJS),
);

const { error } = useReadonlyVersion(client, "aRepository", "aVersionHash");

watchEffect(() => {
if (error.value !== null) {
expect(error.value).toBe("Could not connect.");
done();
}
});
});

describe("does not try loading", () => {
const loadReadonlyVersion = jest.fn((repositoryId, versionHash) =>
Promise.resolve(
new SuccessfulVersionInformationWithModelTree(
versionHash,
) as unknown as VersionInformationWithModelTree,
),
);

let client: Ref<ClientJS | null>;
class MockClientJS {
loadReadonlyVersion(
_repositoryId: string,
_versionHash: string,
): Promise<VersionInformationWithModelTree> {
return loadReadonlyVersion(_repositoryId, _versionHash);
}
}

beforeEach(() => {
jest.clearAllMocks();
client = useModelClient("anURL", () =>
Promise.resolve(new MockClientJS() as unknown as ClientJS),
).client;
});

test("if client is undefined", () => {
useReadonlyVersion(undefined, "aRepository", "aVersionHash");
expect(loadReadonlyVersion).not.toHaveBeenCalled();
});

test("if repositoryId is undefined", () => {
useReadonlyVersion(client, undefined, "aVersionHash");
expect(loadReadonlyVersion).not.toHaveBeenCalled();
});

test("if branchId is undefined", () => {
useReadonlyVersion(client, "aRepository", undefined);
expect(loadReadonlyVersion).not.toHaveBeenCalled();
});
});
94 changes: 94 additions & 0 deletions vue-model-api/src/useReadonlyVersion.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
import type { org } from "@modelix/model-client";
import type { INodeJS } from "@modelix/ts-model-api";
import { useLastPromiseEffect } from "./internal/useLastPromiseEffect";
import type { MaybeRefOrGetter, Ref } from "vue";
import { shallowRef, toValue } from "vue";
import type { ReactiveINodeJS } from "./internal/ReactiveINodeJS";
import { toReactiveINodeJS } from "./internal/ReactiveINodeJS";
import { Cache } from "./internal/Cache";
import { handleChange } from "./internal/handleChange";

type ClientJS = org.modelix.model.client2.ClientJS;
type ChangeJS = org.modelix.model.client2.ChangeJS;
type VersionInformationJS = org.modelix.model.client2.VersionInformationJS;

function isDefined<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}

/**
*
* */
export function useReadonlyVersion(
client: MaybeRefOrGetter<ClientJS | null | undefined>,
repositoryId: MaybeRefOrGetter<string | null | undefined>,
versionHash: MaybeRefOrGetter<string | null | undefined>,
): {
versionInformation: Ref<VersionInformationJS | null>;
rootNode: Ref<INodeJS | null>;
dispose: () => void;
error: Ref<unknown>;
} {
const versionInformationRef: Ref<VersionInformationJS | null> =
shallowRef(null);
const rootNodeRef: Ref<INodeJS | null> = shallowRef(null);
const errorRef: Ref<unknown> = shallowRef(null);

const dispose = () => {
versionInformationRef.value = null;
rootNodeRef.value = null;
errorRef.value = null;
};

useLastPromiseEffect(
() => {
dispose();
const clientValue = toValue(client);
if (!isDefined(clientValue)) {
return;
}
const repositoryIdValue = toValue(repositoryId);
if (!isDefined(repositoryIdValue)) {
return;
}
const versionHashValue = toValue(versionHash);
if (!isDefined(versionHashValue)) {
return;
}
const cache = new Cache<ReactiveINodeJS>();
return clientValue
.loadReadonlyVersion(repositoryIdValue, versionHashValue)
.then(({ version, tree }) => ({
versionInfo: version,
tree,
cache,
}));
},
({ versionInfo, tree, cache }, isResultOfLastStartedPromise) => {
if (isResultOfLastStartedPromise) {
tree.addListener((change: ChangeJS) => {
if (cache === null) {
throw Error("The cache is unexpectedly not set up.");
}
handleChange(change, cache);
});
const unreactiveRootNode = tree.rootNode;
const reactiveRootNode = toReactiveINodeJS(unreactiveRootNode, cache);
versionInformationRef.value = versionInfo;
rootNodeRef.value = reactiveRootNode;
}
},
(reason, isResultOfLastStartedPromise) => {
if (isResultOfLastStartedPromise) {
errorRef.value = reason;
}
},
);

return {
versionInformation: versionInformationRef,
rootNode: rootNodeRef,
dispose,
error: errorRef,
};
}
Loading