Skip to content

Commit 9593046

Browse files
committed
feat(model-client): load readonly version in js client
- added useReadonlyVersion to use it as composable
1 parent b1e8556 commit 9593046

File tree

4 files changed

+254
-0
lines changed

4 files changed

+254
-0
lines changed

model-client/src/jsMain/kotlin/org/modelix/model/client2/ClientJS.kt

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,18 +8,22 @@ import kotlinx.coroutines.DelicateCoroutinesApi
88
import kotlinx.coroutines.GlobalScope
99
import kotlinx.coroutines.await
1010
import kotlinx.coroutines.promise
11+
import kotlinx.datetime.toJSDate
1112
import org.modelix.datastructures.model.IGenericModelTree
1213
import org.modelix.model.TreeId
1314
import org.modelix.model.api.INode
1415
import org.modelix.model.api.INodeReference
1516
import org.modelix.model.api.JSNodeConverter
1617
import org.modelix.model.client.IdGenerator
1718
import org.modelix.model.data.ModelData
19+
import org.modelix.model.lazy.CLVersion
1820
import org.modelix.model.lazy.RepositoryId
1921
import org.modelix.model.lazy.createObjectStoreCache
2022
import org.modelix.model.mutable.DummyIdGenerator
2123
import org.modelix.model.mutable.INodeIdGenerator
2224
import org.modelix.model.mutable.ModelixIdGenerator
25+
import org.modelix.model.mutable.VersionedModelTree
26+
import org.modelix.model.mutable.asMutableSingleThreaded
2327
import org.modelix.model.mutable.asMutableThreadSafe
2428
import org.modelix.model.mutable.load
2529
import org.modelix.model.mutable.withAutoTransactions
@@ -80,6 +84,9 @@ sealed class IdSchemeJS() {
8084
object READONLY : IdSchemeJS()
8185
}
8286

87+
@JsExport
88+
data class VersionInformationWithModelTree(val version: VersionInformationJS, val tree: MutableModelTreeJs)
89+
8390
/**
8491
* JS-API for [ModelClientV2].
8592
* Can be used to perform operations on the model server and to read and write model data.
@@ -111,6 +118,8 @@ interface ClientJS {
111118
*/
112119
fun initRepository(repositoryId: String, useRoleIds: Boolean = true): Promise<Unit>
113120

121+
fun loadReadonlyVersion(repositoryId: String, versionHash: String): Promise<VersionInformationWithModelTree>
122+
114123
/**
115124
* Fetch existing branches for a given repository from the model server.
116125
*
@@ -192,6 +201,19 @@ internal class ClientJSImpl(private val modelClient: ModelClientV2) : ClientJS {
192201
}
193202
}
194203

204+
override fun loadReadonlyVersion(repositoryId: String, versionHash: String): Promise<VersionInformationWithModelTree> {
205+
return GlobalScope.promise {
206+
val version = modelClient.loadVersion(RepositoryId(repositoryId), versionHash, null)
207+
VersionInformationWithModelTree(
208+
VersionInformationJS(
209+
(version as CLVersion).author,
210+
version.getTimestamp()?.toJSDate(),
211+
version.getContentHash()),
212+
MutableModelTreeJsImpl(VersionedModelTree(version).withAutoTransactions())
213+
)
214+
}
215+
}
216+
195217
override fun dispose() {
196218
modelClient.close()
197219
}

vue-model-api/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
export { useModelsFromJson } from "./useModelsFromJson";
22
export { useModelClient } from "./useModelClient";
33
export { useReplicatedModel } from "./useReplicatedModel";
4+
export { useReadonlyVersion } from "./useReadonlyVersion";
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { org } from "@modelix/model-client";
2+
import { INodeJS, toRoleJS } from "@modelix/ts-model-api";
3+
import { watchEffect, type Ref, ref } from "vue";
4+
import { useModelClient } from "./useModelClient";
5+
import { useReadonlyVersion } from "./useReadonlyVersion";
6+
const { loadModelsFromJson } = org.modelix.model.client2;
7+
8+
type VersionInformationWithModelTree =
9+
org.modelix.model.client2.VersionInformationWithModelTree;
10+
type MutableModelTreeJs = org.modelix.model.client2.MutableModelTreeJs;
11+
type ClientJS = org.modelix.model.client2.ClientJS;
12+
type VersionInformationJS = org.modelix.model.client2.VersionInformationJS;
13+
14+
class SuccessfulVersionInformationWithModelTree {
15+
public version: VersionInformationJS;
16+
public tree: MutableModelTreeJs;
17+
18+
constructor(versionHash: string) {
19+
const modelData = {
20+
root: {},
21+
};
22+
23+
const rootNode = loadModelsFromJson([JSON.stringify(modelData)]);
24+
rootNode.setPropertyValue(toRoleJS("versionHash"), versionHash);
25+
this.tree = {
26+
rootNode,
27+
addListener: jest.fn(),
28+
} as unknown as MutableModelTreeJs;
29+
this.version = {
30+
author: "basti",
31+
time: new Date(),
32+
versionHash: "SQnQb*ZS1Vi8Mss2HAu0ENbcpstmqb5E6TuZvpSH1TW4",
33+
} as unknown as VersionInformationJS;
34+
}
35+
36+
addListener = jest.fn();
37+
}
38+
39+
test("test version is loaded", (done) => {
40+
class SuccessfulClientJS {
41+
loadReadonlyVersion(
42+
_repositoryId: string,
43+
_versionHash: string,
44+
): Promise<VersionInformationWithModelTree> {
45+
return Promise.resolve(
46+
new SuccessfulVersionInformationWithModelTree(
47+
_versionHash,
48+
) as unknown as VersionInformationWithModelTree,
49+
);
50+
}
51+
52+
dispose = jest.fn();
53+
}
54+
55+
const { client } = useModelClient("anURL", () =>
56+
Promise.resolve(new SuccessfulClientJS() as unknown as ClientJS),
57+
);
58+
const { rootNode, versionInformation } = useReadonlyVersion(
59+
client,
60+
"aRepository",
61+
"aVersionHash",
62+
);
63+
watchEffect(() => {
64+
if (rootNode.value !== null && versionInformation.value !== null) {
65+
expect(rootNode.value.getPropertyValue(toRoleJS("versionHash"))).toBe(
66+
"aVersionHash",
67+
);
68+
done();
69+
}
70+
});
71+
});
72+
73+
test("test connection error is exposed", (done) => {
74+
class FailingClientJS {
75+
loadReadonlyVersion(
76+
_repositoryId: string,
77+
_versionHash: string,
78+
): Promise<VersionInformationWithModelTree> {
79+
return Promise.reject("Could not connect.");
80+
}
81+
}
82+
83+
const { client } = useModelClient("anURL", () =>
84+
Promise.resolve(new FailingClientJS() as unknown as ClientJS),
85+
);
86+
87+
const { error } = useReadonlyVersion(client, "aRepository", "aVersionHash");
88+
89+
watchEffect(() => {
90+
if (error.value !== null) {
91+
expect(error.value).toBe("Could not connect.");
92+
done();
93+
}
94+
});
95+
});
96+
97+
describe("does not try loading", () => {
98+
const loadReadonlyVersion = jest.fn((repositoryId, versionHash) =>
99+
Promise.resolve(
100+
new SuccessfulVersionInformationWithModelTree(
101+
versionHash,
102+
) as unknown as VersionInformationWithModelTree,
103+
),
104+
);
105+
106+
let client: Ref<ClientJS | null>;
107+
class MockClientJS {
108+
loadReadonlyVersion(
109+
_repositoryId: string,
110+
_versionHash: string,
111+
): Promise<VersionInformationWithModelTree> {
112+
return loadReadonlyVersion(_repositoryId, _versionHash);
113+
}
114+
}
115+
116+
beforeEach(() => {
117+
jest.clearAllMocks();
118+
client = useModelClient("anURL", () =>
119+
Promise.resolve(new MockClientJS() as unknown as ClientJS),
120+
).client;
121+
});
122+
123+
test("if client is undefined", () => {
124+
useReadonlyVersion(undefined, "aRepository", "aVersionHash");
125+
expect(loadReadonlyVersion).not.toHaveBeenCalled();
126+
});
127+
128+
test("if repositoryId is undefined", () => {
129+
useReadonlyVersion(client, undefined, "aVersionHash");
130+
expect(loadReadonlyVersion).not.toHaveBeenCalled();
131+
});
132+
133+
test("if branchId is undefined", () => {
134+
useReadonlyVersion(client, "aRepository", undefined);
135+
expect(loadReadonlyVersion).not.toHaveBeenCalled();
136+
});
137+
});
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
import type { org } from "@modelix/model-client";
2+
import type { INodeJS } from "@modelix/ts-model-api";
3+
import { useLastPromiseEffect } from "./internal/useLastPromiseEffect";
4+
import type { MaybeRefOrGetter, Ref } from "vue";
5+
import { shallowRef, toValue } from "vue";
6+
import type { ReactiveINodeJS } from "./internal/ReactiveINodeJS";
7+
import { toReactiveINodeJS } from "./internal/ReactiveINodeJS";
8+
import { Cache } from "./internal/Cache";
9+
import { handleChange } from "./internal/handleChange";
10+
11+
type ClientJS = org.modelix.model.client2.ClientJS;
12+
type ChangeJS = org.modelix.model.client2.ChangeJS;
13+
type VersionInformationJS = org.modelix.model.client2.VersionInformationJS;
14+
15+
function isDefined<T>(value: T | null | undefined): value is T {
16+
return value !== null && value !== undefined;
17+
}
18+
19+
/**
20+
*
21+
* */
22+
export function useReadonlyVersion(
23+
client: MaybeRefOrGetter<ClientJS | null | undefined>,
24+
repositoryId: MaybeRefOrGetter<string | null | undefined>,
25+
versionHash: MaybeRefOrGetter<string | null | undefined>,
26+
): {
27+
versionInformation: Ref<VersionInformationJS | null>;
28+
rootNode: Ref<INodeJS | null>;
29+
dispose: () => void;
30+
error: Ref<unknown>;
31+
} {
32+
const versionInformationRef: Ref<VersionInformationJS | null> =
33+
shallowRef(null);
34+
const rootNodeRef: Ref<INodeJS | null> = shallowRef(null);
35+
const errorRef: Ref<unknown> = shallowRef(null);
36+
37+
const dispose = () => {
38+
versionInformationRef.value = null;
39+
rootNodeRef.value = null;
40+
errorRef.value = null;
41+
};
42+
43+
useLastPromiseEffect(
44+
() => {
45+
dispose();
46+
const clientValue = toValue(client);
47+
if (!isDefined(clientValue)) {
48+
return;
49+
}
50+
const repositoryIdValue = toValue(repositoryId);
51+
if (!isDefined(repositoryIdValue)) {
52+
return;
53+
}
54+
const versionHashValue = toValue(versionHash);
55+
if (!isDefined(versionHashValue)) {
56+
return;
57+
}
58+
const cache = new Cache<ReactiveINodeJS>();
59+
return clientValue
60+
.loadReadonlyVersion(repositoryIdValue, versionHashValue)
61+
.then(({ version, tree }) => ({
62+
versionInfo: version,
63+
tree,
64+
cache,
65+
}));
66+
},
67+
({ versionInfo, tree, cache }, isResultOfLastStartedPromise) => {
68+
if (isResultOfLastStartedPromise) {
69+
tree.addListener((change: ChangeJS) => {
70+
if (cache === null) {
71+
throw Error("The cache is unexpectedly not set up.");
72+
}
73+
handleChange(change, cache);
74+
});
75+
const unreactiveRootNode = tree.rootNode;
76+
const reactiveRootNode = toReactiveINodeJS(unreactiveRootNode, cache);
77+
versionInformationRef.value = versionInfo;
78+
rootNodeRef.value = reactiveRootNode;
79+
}
80+
},
81+
(reason, isResultOfLastStartedPromise) => {
82+
if (isResultOfLastStartedPromise) {
83+
errorRef.value = reason;
84+
}
85+
},
86+
);
87+
88+
return {
89+
versionInformation: versionInformationRef,
90+
rootNode: rootNodeRef,
91+
dispose,
92+
error: errorRef,
93+
};
94+
}

0 commit comments

Comments
 (0)