From beecb23c79bb45285b5f95a58c8dbaa98470e846 Mon Sep 17 00:00:00 2001 From: Andrey Sobolev Date: Mon, 6 Dec 2021 23:57:35 +0700 Subject: [PATCH] Allow Use client from client-resources from NodeJS (#545) Signed-off-by: Andrey Sobolev --- common/config/rush/pnpm-lock.yaml | 26 ++++++++++----- dev/client-resources/src/connection.ts | 12 ++++--- dev/tool/package.json | 11 +++--- dev/tool/src/connect.ts | 24 +++++++++++++ dev/tool/src/index.ts | 35 ++++++------------- dev/tool/src/workspace.ts | 23 ++++++------- packages/core/src/__tests__/connection.ts | 10 +++--- packages/core/src/client.ts | 16 +++++++-- packages/presentation/src/utils.ts | 39 ++++++++++++---------- packages/query/src/__tests__/connection.ts | 3 +- packages/query/src/index.ts | 4 +++ plugins/client-resources/readme.md | 33 ++++++++++++++++++ plugins/client-resources/src/connection.ts | 30 +++++++++++------ plugins/client/src/index.ts | 22 +++++++++++- plugins/devmodel-resources/src/index.ts | 4 +++ server/mongo/src/__tests__/storage.test.ts | 8 ++++- 16 files changed, 207 insertions(+), 93 deletions(-) create mode 100644 dev/tool/src/connect.ts create mode 100644 plugins/client-resources/readme.md diff --git a/common/config/rush/pnpm-lock.yaml b/common/config/rush/pnpm-lock.yaml index 655da37e6a..3237df8c35 100644 --- a/common/config/rush/pnpm-lock.yaml +++ b/common/config/rush/pnpm-lock.yaml @@ -112,7 +112,6 @@ specifiers: '@types/minio': ^7.0.10 '@types/toposort': ^2.0.3 '@types/uuid': ^8.3.1 - '@types/ws': ^7.4.7 '@typescript-eslint/eslint-plugin': ^5.4.0 autoprefixer: ^10.2.6 commander: ^8.1.0 @@ -272,7 +271,6 @@ dependencies: '@types/minio': 7.0.10 '@types/toposort': 2.0.3 '@types/uuid': 8.3.1 - '@types/ws': 7.4.7 '@typescript-eslint/eslint-plugin': 5.4.0_eslint@7.32.0+typescript@4.4.3 autoprefixer: 10.3.7_postcss@8.3.9 commander: 8.2.0 @@ -1816,6 +1814,12 @@ packages: '@types/node': 16.10.3 dev: false + /@types/ws/8.2.1: + resolution: {integrity: sha512-SqQ+LhVZaJi7c7sYVkjWALDigi/Wy7h7Iu72gkQp8Y8OWw/DddEVBrTSKu86pQftV2+Gm8lYM61hadPKqyaIeg==} + dependencies: + '@types/node': 16.10.3 + dev: false + /@types/yargs-parser/20.2.1: resolution: {integrity: sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==} dev: false @@ -9733,7 +9737,7 @@ packages: dev: false file:projects/chunter.tgz: - resolution: {integrity: sha512-D/Ar+jZ9VwOidVBlO8Kh18DjmojP9ILhHojkN4NsoZ9t1ZoJ8/qiiq3TrEm81KHDqb1NyOm+Bcr8TlhkjchB2g==, tarball: file:projects/chunter.tgz} + resolution: {integrity: sha512-xOX07B2Da1f9nijib0VIqW0GNHn3xNwZI3vr7djeIXyvjLCOIXxTATPF0J6q36/AApKTG5SQyRpiFQ1InU9pHA==, tarball: file:projects/chunter.tgz} name: '@rush-temp/chunter' version: 0.0.0 dependencies: @@ -9799,7 +9803,7 @@ packages: dev: false file:projects/contact-resources.tgz_476f694f64637160ae71e12ff57815b9: - resolution: {integrity: sha512-7wNt7czMMNeogqSzQY1zCG/iOrTKKY6R52gyFcBxxBGUJtRWj7b7s8JtJ137tP0jZTcLlzhd99utKewQ/BkxQQ==, tarball: file:projects/contact-resources.tgz} + resolution: {integrity: sha512-I563510HapGECL9TC1qo8OQsLhKaMl/ttdIWN6vL+XOPcJY+YnmLrMmo0UNMcbCo8liNfvrNmjgbnjpSdl2ikg==, tarball: file:projects/contact-resources.tgz} id: file:projects/contact-resources.tgz name: '@rush-temp/contact-resources' version: 0.0.0 @@ -10250,7 +10254,7 @@ packages: dev: false file:projects/model-chunter.tgz_typescript@4.4.3: - resolution: {integrity: sha512-iB/GGhZOSuGpbKDb2jrxcj5dvndn1Hy61BV3dSxrZf/lBCoaA4SZfd66gWsdBsv17AOMmaWUvFJxxR5S6J2yfg==, tarball: file:projects/model-chunter.tgz} + resolution: {integrity: sha512-NoUF0E2WHE7tfG42VyE11efk61UzUHi0Y2pv4QziQVN6uskIzrnS3xUbSRPyEYff9HvffxDJEB5+Ac73rtuPZQ==, tarball: file:projects/model-chunter.tgz} id: file:projects/model-chunter.tgz name: '@rush-temp/model-chunter' version: 0.0.0 @@ -10271,7 +10275,7 @@ packages: dev: false file:projects/model-contact.tgz_typescript@4.4.3: - resolution: {integrity: sha512-85D68dBZHoPJbRnEythE2CGzp1qbwJ3ynLnBaf3TtNvlgukH1NT15fp2zD8W/4RK9QIscv65yO9gAjTeqjxHUw==, tarball: file:projects/model-contact.tgz} + resolution: {integrity: sha512-qZPBOQC2Oi/Qxpjt91OM1tGBPwShe2SLIHKmDNGSsk79zzdmW3+ZDrge4VVrK/ngc5P2fJQwpQRXgOkNgz2vnA==, tarball: file:projects/model-contact.tgz} id: file:projects/model-contact.tgz name: '@rush-temp/model-contact' version: 0.0.0 @@ -10334,7 +10338,7 @@ packages: dev: false file:projects/model-recruit.tgz_typescript@4.4.3: - resolution: {integrity: sha512-Ja9/9aH8v2JSQzsEtuxG7DFOOB405EoASN2ex9S31NYhYqgIqM8ZaRi5McthYz1e4gkyWFWtOCTHbBccNNOvLg==, tarball: file:projects/model-recruit.tgz} + resolution: {integrity: sha512-LgqXtVecQ6ZdlAb4ufvWI+kDIeeMFtm/5mXvUXBbGsmg07rCa4tsqfm9uwecj6TvpaPYC3mlyYv/N316VeKeGA==, tarball: file:projects/model-recruit.tgz} id: file:projects/model-recruit.tgz name: '@rush-temp/model-recruit' version: 0.0.0 @@ -10479,7 +10483,7 @@ packages: dev: false file:projects/model-task.tgz_typescript@4.4.3: - resolution: {integrity: sha512-AjQvZTFE3GR6hQkU4jFA9M0bJhQhiiBIaZdW+2ALaj3xwAzolD6eSStCUWVWIZgj9uJTSfT8oGTiIw+iISB61Q==, tarball: file:projects/model-task.tgz} + resolution: {integrity: sha512-WzEdSzLqN2Fj+TXMdy9pLEsBPMjqlz6eT2nQLwwQgRyPcT6S6qAbEgqTiPZmk10vW2/qA5X6DDAf82yC/8zVJA==, tarball: file:projects/model-task.tgz} id: file:projects/model-task.tgz name: '@rush-temp/model-task' version: 0.0.0 @@ -11333,7 +11337,7 @@ packages: dev: false file:projects/tool.tgz: - resolution: {integrity: sha512-FFa7jbNSC9W0iRlRnNLYvlhWQUYLgCBbgIFsG9Bw0aWy6s8h1EGfJi5cC4b4z31MprdgXh9egR/cpi29fs7bHg==, tarball: file:projects/tool.tgz} + resolution: {integrity: sha512-klTUXsaa1dlq4wR1u+u3vJ8Vh8Z+StBadUgTZ1r0aqYjVXx9npJFqF+PHucl2LbZ0gDeHqC7crbxfGuui9na1Q==, tarball: file:projects/tool.tgz} name: '@rush-temp/tool' version: 0.0.0 dependencies: @@ -11341,6 +11345,7 @@ packages: '@types/heft-jest': 1.0.2 '@types/minio': 7.0.10 '@types/node': 16.10.3 + '@types/ws': 8.2.1 '@typescript-eslint/eslint-plugin': 5.4.0_87dbf04088b125598d0271706532eaf3 '@typescript-eslint/parser': 5.4.0_eslint@7.32.0+typescript@4.4.3 commander: 8.2.0 @@ -11356,10 +11361,13 @@ packages: prettier: 2.4.1 ts-node: 10.2.1_8304ecd715830f7c190b4d1dea90b100 typescript: 4.4.3 + ws: 8.2.3 transitivePeerDependencies: - '@swc/core' - '@swc/wasm' + - bufferutil - supports-color + - utf-8-validate dev: false file:projects/ui.tgz_476f694f64637160ae71e12ff57815b9: diff --git a/dev/client-resources/src/connection.ts b/dev/client-resources/src/connection.ts index 5ecf0e522b..2a9c57f2c4 100644 --- a/dev/client-resources/src/connection.ts +++ b/dev/client-resources/src/connection.ts @@ -13,14 +13,14 @@ // limitations under the License. // -import type { Tx, Storage, Ref, Doc, Class, DocumentQuery, FindResult, FindOptions, TxHander, ServerStorage, TxResult } from '@anticrm/core' +import type { Class, ClientConnection, Doc, DocumentQuery, FindOptions, FindResult, Ref, ServerStorage, Tx, TxHander, TxResult } from '@anticrm/core' import { DOMAIN_TX } from '@anticrm/core' import { createInMemoryAdapter, createInMemoryTxAdapter } from '@anticrm/dev-storage' -import { createServerStorage, FullTextAdapter, IndexedDoc } from '@anticrm/server-core' +import { protoDeserialize, protoSerialize } from '@anticrm/platform' import type { DbConfiguration } from '@anticrm/server-core' -import { protoSerialize, protoDeserialize } from '@anticrm/platform' +import { createServerStorage, FullTextAdapter, IndexedDoc } from '@anticrm/server-core' -class ServerStorageWrapper implements Storage { +class ServerStorageWrapper implements ClientConnection { constructor (private readonly storage: ServerStorage, private readonly handler: TxHander) {} findAll (_class: Ref>, query: DocumentQuery, options?: FindOptions): Promise> { @@ -34,6 +34,8 @@ class ServerStorageWrapper implements Storage { for (const tx of derived) { this.handler(tx) } return result } + + async close (): Promise {} } class NullFullTextAdapter implements FullTextAdapter { @@ -54,7 +56,7 @@ async function createNullFullTextAdapter (): Promise { return new NullFullTextAdapter() } -export async function connect (handler: (tx: Tx) => void): Promise { +export async function connect (handler: (tx: Tx) => void): Promise { const conf: DbConfiguration = { domains: { [DOMAIN_TX]: 'InMemoryTx' diff --git a/dev/tool/package.json b/dev/tool/package.json index 012817b5b4..b224256e0d 100644 --- a/dev/tool/package.json +++ b/dev/tool/package.json @@ -32,20 +32,23 @@ "eslint-config-standard-with-typescript": "^21.0.1", "prettier": "^2.4.1", "@rushstack/heft": "^0.41.1", - "typescript": "^4.3.5" + "typescript": "^4.3.5", + "@types/ws": "^8.2.1" }, "dependencies": { "mongodb": "^4.1.1", "commander": "^8.1.0", "@anticrm/account": "~0.6.0", "jwt-simple": "^0.5.6", - "@anticrm/contrib": "~0.6.0", "@anticrm/core": "~0.6.11", "@anticrm/contact": "~0.6.2", - "@anticrm/workspace": "~0.6.0", "minio": "^7.0.19", "@anticrm/model-all": "~0.6.0", "@anticrm/model-telegram": "~0.6.0", - "@anticrm/telegram": "~0.6.0" + "@anticrm/telegram": "~0.6.0", + "@anticrm/client-resources": "~0.6.4", + "ws": "^8.2.0", + "@anticrm/client": "~0.6.1", + "@anticrm/platform": "~0.6.5" } } diff --git a/dev/tool/src/connect.ts b/dev/tool/src/connect.ts new file mode 100644 index 0000000000..ae267fb163 --- /dev/null +++ b/dev/tool/src/connect.ts @@ -0,0 +1,24 @@ + +import client from '@anticrm/client' +import clientResources from '@anticrm/client-resources' +import core, { TxOperations } from '@anticrm/core' +import { setMetadata } from '@anticrm/platform' +import { encode } from 'jwt-simple' + +// eslint-disable-next-line +const WebSocket = require('ws') + +export async function connect (transactorUrl: string, workspace: string): Promise<{ connection: TxOperations, close: () => Promise}> { + console.log('connecting to transactor...') + const token = encode({ email: 'anticrm@hc.engineering', workspace }, 'secret') + + // We need to override default factory with 'ws' one. + setMetadata(client.metadata.ClientSocketFactory, (url) => new WebSocket(url)) + const connection = await (await clientResources()).function.GetClient(token, transactorUrl) + return { + connection: new TxOperations(connection, core.account.System), + close: async () => { + await connection.close() + } + } +} diff --git a/dev/tool/src/index.ts b/dev/tool/src/index.ts index de5428b86e..362e9f28e1 100644 --- a/dev/tool/src/index.ts +++ b/dev/tool/src/index.ts @@ -14,26 +14,17 @@ // limitations under the License. // -import { program } from 'commander' -import { MongoClient, Db } from 'mongodb' import { - getAccount, - createAccount, - assignWorkspace, - createWorkspace, - ACCOUNT_DB, - dropWorkspace, - dropAccount, - listWorkspaces + ACCOUNT_DB, assignWorkspace, createAccount, createWorkspace, dropAccount, dropWorkspace, getAccount, listWorkspaces } from '@anticrm/account' -import { createContributingClient } from '@anticrm/contrib' -import core, { TxOperations } from '@anticrm/core' -import { encode } from 'jwt-simple' -import { Client } from 'minio' -import { initWorkspace, upgradeWorkspace, dumpWorkspace } from './workspace' - import contact, { combineName } from '@anticrm/contact' +import core from '@anticrm/core' +import { program } from 'commander' +import { Client } from 'minio' +import { Db, MongoClient } from 'mongodb' +import { connect } from './connect' import { clearTelegramHistory } from './telegram' +import { dumpWorkspace, initWorkspace, upgradeWorkspace } from './workspace' const mongodbUri = process.env.MONGO_URL if (mongodbUri === undefined) { @@ -112,28 +103,24 @@ program await assignWorkspace(db, email, workspace) console.log('connecting to transactor...') - const token = encode({ email: 'anticrm@hc.engineering', workspace }, 'secret') - const url = new URL(`/${token}`, transactorUrl) - const contrib = await createContributingClient(url.href) - const txop = new TxOperations(contrib, core.account.System) + const { connection, close } = await connect(transactorUrl, workspace) const name = combineName(account.first, account.last) console.log('create user in target workspace...') - const employee = await txop.createDoc(contact.class.Employee, contact.space.Employee, { + const employee = await connection.createDoc(contact.class.Employee, contact.space.Employee, { name, city: 'Mountain View', channels: [] }) console.log('create account in target workspace...') - await txop.createDoc(contact.class.EmployeeAccount, core.space.Model, { + await connection.createDoc(contact.class.EmployeeAccount, core.space.Model, { email, employee, name }) - - contrib.close() + await close() }) }) diff --git a/dev/tool/src/workspace.ts b/dev/tool/src/workspace.ts index f528a63e62..e79f07e207 100644 --- a/dev/tool/src/workspace.ts +++ b/dev/tool/src/workspace.ts @@ -14,24 +14,22 @@ // limitations under the License. // -import { MongoClient, Document } from 'mongodb' -import core, { DOMAIN_TX, Tx } from '@anticrm/core' -import { createContributingClient } from '@anticrm/contrib' -import { encode } from 'jwt-simple' -import { BucketItem, Client } from 'minio' import contact from '@anticrm/contact' - +import core, { DOMAIN_TX, Tx } from '@anticrm/core' import builder from '@anticrm/model-all' import { existsSync } from 'fs' import { mkdir, writeFile } from 'fs/promises' +import { BucketItem, Client } from 'minio' +import { Document, MongoClient } from 'mongodb' import { join } from 'path' +import { connect } from './connect' const txes = JSON.parse(JSON.stringify(builder.getTxes())) as Tx[] /** * @public */ -export async function initWorkspace (mongoUrl: string, dbName: string, clientUrl: string, minio: Client): Promise { +export async function initWorkspace (mongoUrl: string, dbName: string, transactorUrl: string, minio: Client): Promise { const client = new MongoClient(mongoUrl) try { await client.connect() @@ -47,13 +45,12 @@ export async function initWorkspace (mongoUrl: string, dbName: string, clientUrl console.log('creating data...') const data = txes.filter((tx) => tx.objectSpace !== core.space.Model) - const token = encode({ email: 'anticrm@hc.engineering', workspace: dbName }, 'secret') - const url = new URL(`/${token}`, clientUrl) - const contrib = await createContributingClient(url.href) + + const { connection, close } = await connect(transactorUrl, dbName) for (const tx of data) { - await contrib.tx(tx) + await connection.tx(tx) } - contrib.close() + await close() console.log('create minio bucket') if (!(await minio.bucketExists(dbName))) { @@ -70,7 +67,7 @@ export async function initWorkspace (mongoUrl: string, dbName: string, clientUrl export async function upgradeWorkspace ( mongoUrl: string, dbName: string, - clientUrl: string, + transactorUrl: string, minio: Client ): Promise { const client = new MongoClient(mongoUrl) diff --git a/packages/core/src/__tests__/connection.ts b/packages/core/src/__tests__/connection.ts index 6c5330fc3c..c38b3edb2c 100644 --- a/packages/core/src/__tests__/connection.ts +++ b/packages/core/src/__tests__/connection.ts @@ -13,16 +13,17 @@ // limitations under the License. // -import type { Storage, DocumentQuery, FindResult, TxResult } from '../storage' import type { Class, Doc, Ref } from '../classes' -import type { Tx } from '../tx' +import { ClientConnection } from '../client' import core from '../component' import { Hierarchy } from '../hierarchy' import { ModelDb, TxDb } from '../memdb' +import type { DocumentQuery, FindResult, TxResult } from '../storage' +import type { Tx } from '../tx' import { DOMAIN_TX } from '../tx' import { genMinModel } from './minmodel' -export async function connect (handler: (tx: Tx) => void): Promise { +export async function connect (handler: (tx: Tx) => void): Promise { const txes = genMinModel() const hierarchy = new Hierarchy() @@ -50,6 +51,7 @@ export async function connect (handler: (tx: Tx) => void): Promise { const result = await Promise.all([model.tx(tx), transactions.tx(tx)]) return result[0] // handler(tx) - we have only one client, should not update? - } + }, + close: async () => {} } } diff --git a/packages/core/src/client.ts b/packages/core/src/client.ts index 6873be7a2d..a8d1f97ddf 100644 --- a/packages/core/src/client.ts +++ b/packages/core/src/client.ts @@ -41,12 +41,20 @@ export interface Client extends Storage { query: DocumentQuery, options?: FindOptions ) => Promise | undefined> + close: () => Promise +} + +/** + * @public + */ +export interface ClientConnection extends Storage { + close: () => Promise } class ClientImpl implements Client { notify?: (tx: Tx) => void - constructor (private readonly hierarchy: Hierarchy, private readonly model: ModelDb, private readonly conn: Storage) { + constructor (private readonly hierarchy: Hierarchy, private readonly model: ModelDb, private readonly conn: ClientConnection) { } getHierarchy (): Hierarchy { return this.hierarchy } @@ -90,13 +98,17 @@ class ClientImpl implements Client { } this.notify?.(tx) } + + async close (): Promise { + await this.conn.close() + } } /** * @public */ export async function createClient ( - connect: (txHandler: TxHander) => Promise + connect: (txHandler: TxHander) => Promise ): Promise { let client: ClientImpl | null = null let txBuffer: Tx[] | undefined = [] diff --git a/packages/presentation/src/utils.ts b/packages/presentation/src/utils.ts index 7c6239754d..6712bd3d18 100644 --- a/packages/presentation/src/utils.ts +++ b/packages/presentation/src/utils.ts @@ -1,15 +1,15 @@ // // Copyright © 2020, 2021 Anticrm Platform Contributors. // Copyright © 2021 Hardcore Engineering Inc. -// +// // Licensed under the Eclipse Public License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. You may // obtain a copy of the License at https://www.eclipse.org/legal/epl-2.0 -// +// // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// +// // See the License for the specific language governing permissions and // limitations under the License. // @@ -19,7 +19,7 @@ import { onDestroy } from 'svelte' import { Doc, Ref, Class, DocumentQuery, FindOptions, Client, Hierarchy, Tx, getCurrentAccount, ModelDb, TxResult, TxOperations, AnyAttribute, RefTo } from '@anticrm/core' import core from '@anticrm/core' import { LiveQuery as LQ } from '@anticrm/query' -import { getMetadata } from '@anticrm/platform' +import { getMetadata } from '@anticrm/platform' import login from '@anticrm/login' @@ -27,12 +27,11 @@ let liveQuery: LQ let client: Client & TxOperations class UIClient extends TxOperations implements Client { - constructor (private readonly client: Client, private readonly liveQuery: LQ) { super(client, getCurrentAccount()._id) } - getHierarchy (): Hierarchy { + getHierarchy (): Hierarchy { return this.client.getHierarchy() } @@ -40,53 +39,57 @@ class UIClient extends TxOperations implements Client { return this.client.getModel() } - tx(tx: Tx): Promise { + async tx (tx: Tx): Promise { // return Promise.all([super.tx(tx), this.liveQuery.tx(tx)]) as unknown as Promise - return super.tx(tx) + return await super.tx(tx) + } + + async close (): Promise { + await client.close() } } -export function getClient(): Client & TxOperations { +export function getClient (): Client & TxOperations { return client } -export function setClient(_client: Client) { +export function setClient (_client: Client): void { liveQuery = new LQ(_client) client = new UIClient(_client, liveQuery) _client.notify = (tx: Tx) => { - liveQuery.tx(tx) + liveQuery.tx(tx).catch(err => console.log(err)) } } export class LiveQuery { private unsubscribe = () => {} - constructor() { + constructor () { onDestroy(() => { console.log('onDestroy query'); this.unsubscribe() }) } - query(_class: Ref>, query: DocumentQuery, callback: (result: T[]) => void, options?: FindOptions) { + query(_class: Ref>, query: DocumentQuery, callback: (result: T[]) => void, options?: FindOptions): void { this.unsubscribe() this.unsubscribe = liveQuery.query(_class, query, callback, options) } } -export function createQuery() { return new LiveQuery() } +export function createQuery (): LiveQuery { return new LiveQuery() } -export function getFileUrl(file: string): string { +export function getFileUrl (file: string): string { const uploadUrl = getMetadata(login.metadata.UploadUrl) const token = getMetadata(login.metadata.LoginToken) - const url = `${uploadUrl}?file=${file}&token=${token}` + const url = `${uploadUrl as string}?file=${file}&token=${token as string}` return url } /** * @public */ - export function getAttributePresenterClass (attribute: AnyAttribute): Ref> { +export function getAttributePresenterClass (attribute: AnyAttribute): Ref> { let attrClass = attribute.type._class if (attrClass === core.class.RefTo) { attrClass = (attribute.type as RefTo).to } return attrClass -} \ No newline at end of file +} diff --git a/packages/query/src/__tests__/connection.ts b/packages/query/src/__tests__/connection.ts index 87cc119013..92f0e09a3a 100644 --- a/packages/query/src/__tests__/connection.ts +++ b/packages/query/src/__tests__/connection.ts @@ -53,6 +53,7 @@ export async function connect (handler: (tx: Tx) => void): Promise { // Not required, since handled in client. // handler(tx) return {} - } + }, + close: async () => {} } } diff --git a/packages/query/src/index.ts b/packages/query/src/index.ts index f1543a69ab..4a5e7ba6bc 100644 --- a/packages/query/src/index.ts +++ b/packages/query/src/index.ts @@ -65,6 +65,10 @@ export class LiveQuery extends TxProcessor implements Client { this.client = client } + async close (): Promise { + return await this.client.close() + } + getHierarchy (): Hierarchy { return this.client.getHierarchy() } diff --git a/plugins/client-resources/readme.md b/plugins/client-resources/readme.md new file mode 100644 index 0000000000..1c169c21a9 --- /dev/null +++ b/plugins/client-resources/readme.md @@ -0,0 +1,33 @@ +# Overview + +Package allow to create a client to interact with running platform. + +## Usage + +```ts + import clientResources from '@anticrm/client-resources' + import core, { Client } from '@anticrm/core' + + // ... + + const token = ... // Token obtained somehow. + + const connection: Client = await (await clientResources()).function.GetClient(token, transactorUrl) + + // Now client is usable + + // Use close, to shutdown connection. + await connection.close() +``` + +## Node JS + +For NodeJS enviornment it is required to configure ClientSocketFactory using 'ws' package. + +```ts +// We need to override default WebSocket factory with 'ws' one. +setMetadata(client.metadata.ClientSocketFactory, (url) => new WebSocket(url)) + +const connection: Client = await (await clientResources()).function.GetClient(token, transactorUrl) +... +``` diff --git a/plugins/client-resources/src/connection.ts b/plugins/client-resources/src/connection.ts index 380c6b8b0a..451cee932e 100644 --- a/plugins/client-resources/src/connection.ts +++ b/plugins/client-resources/src/connection.ts @@ -14,9 +14,9 @@ // limitations under the License. // -import type { Class, Doc, DocumentQuery, FindOptions, FindResult, Ref, Storage, Tx, TxHander, TxResult } from '@anticrm/core' -import type { ReqId } from '@anticrm/platform' -import { serialize, readResponse } from '@anticrm/platform' +import client, { ClientSocket } from '@anticrm/client' +import type { Class, ClientConnection, Doc, DocumentQuery, FindOptions, FindResult, Ref, Tx, TxHander, TxResult } from '@anticrm/core' +import { getMetadata, readResponse, ReqId, serialize } from '@anticrm/platform' class DeferredPromise { readonly promise: Promise @@ -30,22 +30,30 @@ class DeferredPromise { } } -class Connection implements Storage { - private websocket: WebSocket | null = null +class Connection implements ClientConnection { + private websocket: ClientSocket | null = null private readonly requests = new Map() private lastId = 0 + private readonly interval: number constructor (private readonly url: string, private readonly handler: TxHander) { console.log('connection created') - setInterval(() => { - console.log('ping') + this.interval = setInterval(() => { // eslint-disable-next-line @typescript-eslint/no-floating-promises this.sendRequest('ping') }, 10000) } - private openConnection (): Promise { - const websocket = new WebSocket(this.url) + async close (): Promise { + clearInterval(this.interval) + this.websocket?.close() + } + + private openConnection (): Promise { + // Use defined factory or browser default one. + const clientSocketFactory = getMetadata(client.metadata.ClientSocketFactory) ?? ((url: string) => new WebSocket(url) as ClientSocket) + + const websocket = clientSocketFactory(this.url) websocket.onmessage = (event: MessageEvent) => { const resp = readResponse(event.data) if (resp.id !== undefined) { @@ -71,7 +79,7 @@ class Connection implements Storage { websocket.onopen = () => { resolve(websocket) } - websocket.onerror = (event) => { + websocket.onerror = (event: any) => { console.log('client websocket error', event) reject(new Error('websocket error')) } @@ -103,6 +111,6 @@ class Connection implements Storage { /** * @public */ -export async function connect (url: string, handler: TxHander): Promise { +export async function connect (url: string, handler: TxHander): Promise { return new Connection(url, handler) } diff --git a/plugins/client/src/index.ts b/plugins/client/src/index.ts index 49de1e44b8..6b5c1449cb 100644 --- a/plugins/client/src/index.ts +++ b/plugins/client/src/index.ts @@ -30,6 +30,25 @@ export const clientId = 'client' as Plugin */ export type ClientHook = (client: Client) => Promise +/** + * @public + */ +export type ClientSocketFactory = (url: string) => ClientSocket + +/** + * @public + */ +export interface ClientSocket { + onmessage?: ((this: ClientSocket, ev: MessageEvent) => any) | null + onclose?: ((this: ClientSocket, ev: CloseEvent) => any) | null + onopen?: ((this: ClientSocket, ev: Event) => any) | null + onerror?: ((this: ClientSocket, ev: Event) => any) | null + + send: (data: string | ArrayBufferLike | Blob | ArrayBufferView) => void + + close: () => void +} + /** * @public */ @@ -38,7 +57,8 @@ export type ClientFactory = (token: string, endpoint: string) => Promise export default plugin(clientId, { metadata: { - ClientHook: '' as Metadata> + ClientHook: '' as Metadata>, + ClientSocketFactory: '' as Metadata }, function: { GetClient: '' as Resource diff --git a/plugins/devmodel-resources/src/index.ts b/plugins/devmodel-resources/src/index.ts index d728413e18..1aef36032f 100644 --- a/plugins/devmodel-resources/src/index.ts +++ b/plugins/devmodel-resources/src/index.ts @@ -78,6 +78,10 @@ class ModelClient implements Client { transactions.push({ tx, result }) return result } + + async close (): Promise { + await this.client.close() + } } export async function Hook (client: Client): Promise { console.info('devmodel# Client HOOKED by DevModel') diff --git a/server/mongo/src/__tests__/storage.test.ts b/server/mongo/src/__tests__/storage.test.ts index eeb87125c1..59b118852d 100644 --- a/server/mongo/src/__tests__/storage.test.ts +++ b/server/mongo/src/__tests__/storage.test.ts @@ -56,6 +56,8 @@ class NullDbAdapter implements DbAdapter { async tx (tx: Tx): Promise { return {} } + + async close (): Promise {} } async function createNullAdapter (hierarchy: Hierarchy, url: string, db: string, modelDb: ModelDb): Promise { @@ -155,7 +157,11 @@ describe('mongo operations', () => { const serverStorage = await createServerStorage(conf) client = await createClient(async (handler) => { - return await Promise.resolve(serverStorage) + return { + findAll: async (_class, query, options) => await serverStorage.findAll(_class, query, options), + tx: async (tx) => await serverStorage.tx(tx), + close: async () => {} + } }) operations = new TxOperations(client, core.account.System)