remult 0.16.3-exp.4 → 0.16.4
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/esm/remult-knex/index.js +9 -2
- package/esm/server/expressBridge.d.ts +2 -0
- package/esm/server/expressBridge.js +14 -8
- package/esm/server/initAsyncHooks.js +0 -1
- package/esm/src/backend-tests/backend-database-test-setup.backend-spec.d.ts +1 -0
- package/esm/src/backend-tests/backend-database-test-setup.backend-spec.js +10 -0
- package/esm/src/backend-tests/my-test.backend-spec.js +48 -0
- package/esm/src/live-query/EventSourceLiveQuery.d.ts +5 -0
- package/esm/src/live-query/EventSourceLiveQuery.js +48 -0
- package/esm/src/live-query/EventSourceLiveQueryProvider.d.ts +6 -0
- package/esm/src/live-query/EventSourceLiveQueryProvider.js +24 -0
- package/esm/src/live-query/LiveQuery.d.ts +67 -0
- package/esm/src/live-query/LiveQuery.js +222 -0
- package/esm/src/live-query/LiveQueryManager.d.ts +33 -0
- package/esm/src/live-query/LiveQueryManager.js +97 -0
- package/esm/src/remult-proxy.js +4 -0
- package/esm/src/shared-tests/db-tests-setup.d.ts +1 -1
- package/esm/src/shared-tests/db-tests-setup.js +1 -0
- package/esm/src/valueConverters.js +2 -0
- package/package.json +1 -1
- package/remult-knex/index.js +16 -8
- package/server/expressBridge.d.ts +2 -0
- package/server/expressBridge.js +14 -8
- package/server/initAsyncHooks.js +0 -1
- package/src/backend-tests/backend-database-test-setup.backend-spec.d.ts +1 -0
- package/src/backend-tests/backend-database-test-setup.backend-spec.js +11 -1
- package/src/backend-tests/my-test.backend-spec.js +57 -0
- package/src/live-query/EventSourceLiveQuery.d.ts +5 -0
- package/src/live-query/EventSourceLiveQuery.js +62 -0
- package/src/live-query/EventSourceLiveQueryProvider.d.ts +6 -0
- package/src/live-query/EventSourceLiveQueryProvider.js +32 -0
- package/src/live-query/LiveQuery.d.ts +67 -0
- package/src/live-query/LiveQuery.js +305 -0
- package/src/live-query/LiveQueryManager.d.ts +33 -0
- package/src/live-query/LiveQueryManager.js +196 -0
- package/src/remult-proxy.js +4 -0
- package/src/shared-tests/db-tests-setup.d.ts +1 -1
- package/src/shared-tests/db-tests-setup.js +1 -0
- package/src/valueConverters.js +2 -0
package/esm/remult-knex/index.js
CHANGED
|
@@ -153,8 +153,15 @@ class KnexEntityDataProvider {
|
|
|
153
153
|
}
|
|
154
154
|
let insert = this.knex(e.entityName).insert(insertObject);
|
|
155
155
|
if (isAutoIncrement(this.entity.idMetadata.field)) {
|
|
156
|
-
let
|
|
157
|
-
|
|
156
|
+
let newId;
|
|
157
|
+
if (this.knex.client.config.client === 'mysql2') {
|
|
158
|
+
let result = await this.knex(e.entityName).insert(insertObject, this.entity.idMetadata.field.key);
|
|
159
|
+
newId = result[0];
|
|
160
|
+
}
|
|
161
|
+
else {
|
|
162
|
+
let result = await insert.returning(this.entity.idMetadata.field.key);
|
|
163
|
+
newId = result[0].id;
|
|
164
|
+
}
|
|
158
165
|
resultFilter = new Filter(x => x.isEqualTo(this.entity.idMetadata.field, newId));
|
|
159
166
|
}
|
|
160
167
|
else
|
|
@@ -59,6 +59,8 @@ export interface GenericResponse {
|
|
|
59
59
|
}
|
|
60
60
|
export declare class RemultAsyncLocalStorage {
|
|
61
61
|
private readonly remultObjectStorage;
|
|
62
|
+
static enable(): void;
|
|
63
|
+
static disable(): void;
|
|
62
64
|
constructor(remultObjectStorage: import('async_hooks').AsyncLocalStorage<Remult>);
|
|
63
65
|
run(remult: Remult, callback: VoidFunction): void;
|
|
64
66
|
getRemult(): Remult;
|
|
@@ -30,6 +30,7 @@ export function createRemultServer(options) {
|
|
|
30
30
|
return dp;
|
|
31
31
|
return new (await import('./JsonEntityFileStorage')).JsonFileDataProvider('./db');
|
|
32
32
|
});
|
|
33
|
+
RemultAsyncLocalStorage.enable();
|
|
33
34
|
if (options.initApi) {
|
|
34
35
|
dataProvider = dataProvider.then(async (dp) => {
|
|
35
36
|
var remult = new Remult(dp);
|
|
@@ -63,6 +64,18 @@ export class RemultAsyncLocalStorage {
|
|
|
63
64
|
constructor(remultObjectStorage) {
|
|
64
65
|
this.remultObjectStorage = remultObjectStorage;
|
|
65
66
|
}
|
|
67
|
+
static enable() {
|
|
68
|
+
remult.remultFactory = () => {
|
|
69
|
+
const r = RemultAsyncLocalStorage.instance.getRemult();
|
|
70
|
+
if (r)
|
|
71
|
+
return r;
|
|
72
|
+
else
|
|
73
|
+
throw new Error("remult object was requested outside of a valid context, try running it within initApi or a remult request cycle");
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
static disable() {
|
|
77
|
+
remult.resetFactory();
|
|
78
|
+
}
|
|
66
79
|
run(remult, callback) {
|
|
67
80
|
if (this.remultObjectStorage)
|
|
68
81
|
this.remultObjectStorage.run(remult, callback);
|
|
@@ -71,19 +84,12 @@ export class RemultAsyncLocalStorage {
|
|
|
71
84
|
}
|
|
72
85
|
getRemult() {
|
|
73
86
|
if (!this.remultObjectStorage) {
|
|
74
|
-
throw new Error("can't use static remult in this environment, `async_hooks` were not
|
|
87
|
+
throw new Error("can't use static remult in this environment, `async_hooks` were not initialized");
|
|
75
88
|
}
|
|
76
89
|
return this.remultObjectStorage.getStore();
|
|
77
90
|
}
|
|
78
91
|
}
|
|
79
92
|
RemultAsyncLocalStorage.instance = new RemultAsyncLocalStorage(undefined);
|
|
80
|
-
remult.remultFactory = () => {
|
|
81
|
-
const r = RemultAsyncLocalStorage.instance.getRemult();
|
|
82
|
-
if (r)
|
|
83
|
-
return r;
|
|
84
|
-
else
|
|
85
|
-
throw new Error("remult object was requested outside of a valid context, try running it within initApi or a remult request cycle");
|
|
86
|
-
};
|
|
87
93
|
class RemultServerImplementation {
|
|
88
94
|
constructor(queue, options, dataProvider) {
|
|
89
95
|
this.queue = queue;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { dbTestWhatSignature } from "../shared-tests/db-tests-setup";
|
|
2
2
|
export declare const testKnexPGSqlImpl: (key: string, what: dbTestWhatSignature, focus?: boolean) => void;
|
|
3
|
+
export declare const mySqlTest: import("../shared-tests/db-tests-setup").dbTestMethodSignature;
|
|
3
4
|
export declare function testPostgresImplementation(key: string, what: dbTestWhatSignature, focus?: boolean): void;
|
|
4
5
|
export declare function testMongo(key: string, what: dbTestWhatSignature, focus?: boolean): void;
|
|
5
6
|
import '../shared-tests';
|
|
@@ -53,6 +53,16 @@ if (true)
|
|
|
53
53
|
filename: ":memory:"
|
|
54
54
|
},
|
|
55
55
|
}), "sqlite3"));
|
|
56
|
+
export const mySqlTest = addDatabaseToTest(testKnexSqlImpl(Knex.default({
|
|
57
|
+
client: 'mysql2',
|
|
58
|
+
connection: {
|
|
59
|
+
user: 'root',
|
|
60
|
+
password: 'MASTERKEY',
|
|
61
|
+
host: '127.0.0.1',
|
|
62
|
+
database: 'test'
|
|
63
|
+
}
|
|
64
|
+
//debug: true
|
|
65
|
+
}), "mysql2"));
|
|
56
66
|
let pg = createPostgresConnection({
|
|
57
67
|
autoCreateTables: false
|
|
58
68
|
});
|
|
@@ -1,8 +1,12 @@
|
|
|
1
|
+
import { Remult } from "../context";
|
|
1
2
|
import { KnexDataProvider } from '../../remult-knex';
|
|
2
3
|
import { config } from 'dotenv';
|
|
3
4
|
import { testKnexPGSqlImpl, testPostgresImplementation } from "./backend-database-test-setup.backend-spec";
|
|
4
5
|
import { entityWithValidations } from "../shared-tests/entityWithValidations";
|
|
5
6
|
import { SqlDatabase } from "../data-providers/sql-database";
|
|
7
|
+
import { RemultAsyncLocalStorage } from "../../server/expressBridge";
|
|
8
|
+
import { initAsyncHooks } from "../../server/initAsyncHooks";
|
|
9
|
+
import { remult } from "../remult-proxy";
|
|
6
10
|
config();
|
|
7
11
|
testPostgresImplementation("default order by", async ({ createEntity }) => {
|
|
8
12
|
let s = await entityWithValidations.create4RowsInDp(createEntity);
|
|
@@ -57,3 +61,47 @@ testKnexPGSqlImpl("knex filter2", async ({ createEntity }) => {
|
|
|
57
61
|
}
|
|
58
62
|
})).length).toBe(3);
|
|
59
63
|
}, false);
|
|
64
|
+
it("test async hooks and static remult", async () => {
|
|
65
|
+
let gotException = true;
|
|
66
|
+
try {
|
|
67
|
+
RemultAsyncLocalStorage.instance.getRemult();
|
|
68
|
+
gotException = false;
|
|
69
|
+
}
|
|
70
|
+
catch { }
|
|
71
|
+
expect(gotException).toBe(true);
|
|
72
|
+
initAsyncHooks();
|
|
73
|
+
expect(RemultAsyncLocalStorage.instance.getRemult()).toBe(undefined);
|
|
74
|
+
RemultAsyncLocalStorage.enable();
|
|
75
|
+
try {
|
|
76
|
+
remult.isAllowed(false);
|
|
77
|
+
gotException = false;
|
|
78
|
+
}
|
|
79
|
+
catch { }
|
|
80
|
+
expect(gotException).toBe(true);
|
|
81
|
+
const promises = [];
|
|
82
|
+
RemultAsyncLocalStorage.instance.run(new Remult(), () => {
|
|
83
|
+
remult.user = { id: 'noam' };
|
|
84
|
+
promises.push(new Promise(res => {
|
|
85
|
+
setTimeout(() => {
|
|
86
|
+
expect(remult.user.id).toBe('noam');
|
|
87
|
+
res({});
|
|
88
|
+
}, 10);
|
|
89
|
+
}));
|
|
90
|
+
RemultAsyncLocalStorage.instance.run(new Remult(), () => {
|
|
91
|
+
remult.user = { id: 'yoni' };
|
|
92
|
+
promises.push(new Promise(res => {
|
|
93
|
+
setTimeout(() => {
|
|
94
|
+
expect(remult.user.id).toBe('yoni');
|
|
95
|
+
res({});
|
|
96
|
+
}, 10);
|
|
97
|
+
}));
|
|
98
|
+
});
|
|
99
|
+
promises.push(new Promise(res => {
|
|
100
|
+
setTimeout(() => {
|
|
101
|
+
expect(remult.user.id).toBe('noam');
|
|
102
|
+
res({});
|
|
103
|
+
}, 10);
|
|
104
|
+
}));
|
|
105
|
+
});
|
|
106
|
+
await Promise.all(promises);
|
|
107
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { LiveQueryClient, streamUrl } from './LiveQuery';
|
|
2
|
+
import { fetchEventSource } from '@microsoft/fetch-event-source';
|
|
3
|
+
import { remult } from '../remult-proxy';
|
|
4
|
+
export class EventSourceLiveQuery extends LiveQueryClient {
|
|
5
|
+
constructor(wrapMessage) {
|
|
6
|
+
super(new EventSourceLiveQueryProvider(wrapMessage));
|
|
7
|
+
this.wrapMessage = wrapMessage;
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
class EventSourceLiveQueryProvider {
|
|
11
|
+
constructor(wrapMessage, url = undefined, jwtToken) {
|
|
12
|
+
this.wrapMessage = wrapMessage;
|
|
13
|
+
this.url = url;
|
|
14
|
+
this.jwtToken = jwtToken;
|
|
15
|
+
this.lastId = 0;
|
|
16
|
+
if (!this.wrapMessage)
|
|
17
|
+
this.wrapMessage = x => x();
|
|
18
|
+
if (!this.url) {
|
|
19
|
+
this.url = remult.apiClient.url + '/' + streamUrl;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
openStreamAndReturnCloseFunction(clientId, onMessage) {
|
|
23
|
+
const ctrl = new AbortController();
|
|
24
|
+
const headers = {
|
|
25
|
+
"client-id": clientId
|
|
26
|
+
};
|
|
27
|
+
if (this.jwtToken) {
|
|
28
|
+
headers["Authorization"] = "Bearer " + this.jwtToken;
|
|
29
|
+
}
|
|
30
|
+
fetchEventSource(this.url, {
|
|
31
|
+
headers,
|
|
32
|
+
onmessage: message => {
|
|
33
|
+
const mid = +message.id;
|
|
34
|
+
if (mid <= this.lastId && this.lastId - mid < 10)
|
|
35
|
+
return;
|
|
36
|
+
this.lastId = mid;
|
|
37
|
+
console.log(message.data);
|
|
38
|
+
if (message.event !== 'keep-alive') {
|
|
39
|
+
this.wrapMessage(() => onMessage(message));
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
onopen: async () => {
|
|
43
|
+
},
|
|
44
|
+
signal: ctrl.signal,
|
|
45
|
+
});
|
|
46
|
+
return () => ctrl.abort();
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { LiveQueryProvider, MessageHandler } from "./LiveQuery";
|
|
2
|
+
export declare class EventSourceLiveQueryProvider implements LiveQueryProvider {
|
|
3
|
+
private wrapMessage?;
|
|
4
|
+
constructor(wrapMessage?: (what: () => void) => void);
|
|
5
|
+
openStreamAndReturnCloseFunction(clientId: string, onMessage: MessageHandler): Promise<VoidFunction>;
|
|
6
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { remult } from "../remult-proxy";
|
|
2
|
+
import { streamUrl } from "./LiveQuery";
|
|
3
|
+
export class EventSourceLiveQueryProvider {
|
|
4
|
+
constructor(wrapMessage) {
|
|
5
|
+
this.wrapMessage = wrapMessage;
|
|
6
|
+
if (!this.wrapMessage)
|
|
7
|
+
this.wrapMessage = x => x();
|
|
8
|
+
}
|
|
9
|
+
openStreamAndReturnCloseFunction(clientId, onMessage) {
|
|
10
|
+
const source = new EventSource(remult.apiClient.url + '/' + streamUrl + "?id=" + clientId, {
|
|
11
|
+
withCredentials: true
|
|
12
|
+
});
|
|
13
|
+
source.onmessage = e => {
|
|
14
|
+
this.wrapMessage(() => onMessage(JSON.parse(e.data)));
|
|
15
|
+
};
|
|
16
|
+
return new Promise((res) => {
|
|
17
|
+
source.onopen = e => {
|
|
18
|
+
res(() => {
|
|
19
|
+
source.close();
|
|
20
|
+
});
|
|
21
|
+
};
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { EntityOrderBy, FindOptions, Remult, Repository, RestDataProviderHttpProvider } from '../../index';
|
|
2
|
+
import { Allowed } from '../context';
|
|
3
|
+
import { ServerEventDispatcher } from './LiveQueryManager';
|
|
4
|
+
export declare const streamUrl = "stream1";
|
|
5
|
+
export interface LiveQueryProvider {
|
|
6
|
+
openStreamAndReturnCloseFunction(clientId: string, onMessage: MessageHandler): Promise<VoidFunction>;
|
|
7
|
+
}
|
|
8
|
+
export declare type MessageHandler = (message: {
|
|
9
|
+
data: any;
|
|
10
|
+
event: string;
|
|
11
|
+
}) => void;
|
|
12
|
+
export declare class LiveQueryClient {
|
|
13
|
+
lqp: LiveQueryProvider;
|
|
14
|
+
private provider?;
|
|
15
|
+
clientId: any;
|
|
16
|
+
private queries;
|
|
17
|
+
private channels;
|
|
18
|
+
constructor(lqp: LiveQueryProvider, provider?: RestDataProviderHttpProvider);
|
|
19
|
+
runPromise(p: Promise<any>): Promise<any>;
|
|
20
|
+
close(): void;
|
|
21
|
+
subscribeChannel<T>(key: string, onResult: (item: T) => void): () => void;
|
|
22
|
+
private closeIfNoListeners;
|
|
23
|
+
subscribe<entityType>(repo: Repository<entityType>, options: FindOptions<entityType>, onResult: (reducer: (prevState: entityType[]) => entityType[]) => void): () => void;
|
|
24
|
+
closeListener: Promise<VoidFunction>;
|
|
25
|
+
private openIfNoOpened;
|
|
26
|
+
}
|
|
27
|
+
export declare type listener = (message: any) => void;
|
|
28
|
+
export interface SubscribeToQueryArgs<entityType = any> {
|
|
29
|
+
entityKey: string;
|
|
30
|
+
orderBy?: EntityOrderBy<entityType>;
|
|
31
|
+
}
|
|
32
|
+
export declare type liveQueryMessage = {
|
|
33
|
+
type: "all";
|
|
34
|
+
data: any[];
|
|
35
|
+
} | {
|
|
36
|
+
type: "add";
|
|
37
|
+
data: any;
|
|
38
|
+
} | {
|
|
39
|
+
type: 'replace';
|
|
40
|
+
data: {
|
|
41
|
+
oldId: any;
|
|
42
|
+
item: any;
|
|
43
|
+
};
|
|
44
|
+
} | {
|
|
45
|
+
type: "remove";
|
|
46
|
+
data: {
|
|
47
|
+
id: any;
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
export interface SubscribeResult {
|
|
51
|
+
result: [];
|
|
52
|
+
id: string;
|
|
53
|
+
}
|
|
54
|
+
export interface ChannelSubscribe {
|
|
55
|
+
clientId: string;
|
|
56
|
+
channel: string;
|
|
57
|
+
remove: boolean;
|
|
58
|
+
}
|
|
59
|
+
export declare class AMessageChannel<messageType> {
|
|
60
|
+
private subscribedAllowed;
|
|
61
|
+
userCanSubscribe(channel: string, remult: Remult): boolean;
|
|
62
|
+
private key;
|
|
63
|
+
constructor(key: (string | ((remult: Remult) => string)), subscribedAllowed: Allowed);
|
|
64
|
+
send(what: messageType, remult?: Remult): void;
|
|
65
|
+
subscribe(client: LiveQueryClient, onValue: (value: messageType) => void, remult?: Remult): void;
|
|
66
|
+
dispatcher: ServerEventDispatcher;
|
|
67
|
+
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { getEntityRef, remult as defaultRemult, Sort } from '../../index';
|
|
2
|
+
import { v4 as uuid } from 'uuid';
|
|
3
|
+
import { RestEntityDataProvider } from '../data-providers/rest-data-provider';
|
|
4
|
+
import { buildRestDataProvider } from '../context';
|
|
5
|
+
export const streamUrl = 'stream1';
|
|
6
|
+
class LiveQueryOnFrontEnd {
|
|
7
|
+
constructor(repo, query) {
|
|
8
|
+
this.repo = repo;
|
|
9
|
+
this.query = query;
|
|
10
|
+
this.items = [];
|
|
11
|
+
this.listeners = [];
|
|
12
|
+
}
|
|
13
|
+
async setAllItems(result) {
|
|
14
|
+
const items = await Promise.all(result.map(item => this.repo.fromJson(item)));
|
|
15
|
+
this.forListeners(listener => {
|
|
16
|
+
listener(x => {
|
|
17
|
+
return items;
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
forListeners(what) {
|
|
22
|
+
for (const l of this.listeners) {
|
|
23
|
+
what(l);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
async handle(message) {
|
|
27
|
+
const sort = (items) => {
|
|
28
|
+
if (this.query.orderBy) {
|
|
29
|
+
const o = Sort.translateOrderByToSort(this.repo.metadata, this.query.orderBy);
|
|
30
|
+
items.sort((a, b) => o.compare(a, b));
|
|
31
|
+
}
|
|
32
|
+
return items;
|
|
33
|
+
};
|
|
34
|
+
switch (message.type) {
|
|
35
|
+
case "all":
|
|
36
|
+
this.setAllItems(message.data);
|
|
37
|
+
break;
|
|
38
|
+
case "replace": {
|
|
39
|
+
const item = await this.repo.fromJson(message.data.item);
|
|
40
|
+
this.forListeners(listener => {
|
|
41
|
+
listener(items => {
|
|
42
|
+
if (!items)
|
|
43
|
+
items = [];
|
|
44
|
+
return sort(items.map(x => this.repo.getEntityRef(x).getId() === message.data.oldId ? item : x));
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
break;
|
|
48
|
+
}
|
|
49
|
+
case "add":
|
|
50
|
+
{
|
|
51
|
+
const item = await this.repo.fromJson(message.data.item);
|
|
52
|
+
this.forListeners(listener => listener(items => {
|
|
53
|
+
if (!items)
|
|
54
|
+
items = [];
|
|
55
|
+
items.push(item);
|
|
56
|
+
return sort(items);
|
|
57
|
+
}));
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
case "remove":
|
|
61
|
+
this.forListeners(listener => listener(items => {
|
|
62
|
+
if (!items)
|
|
63
|
+
items = [];
|
|
64
|
+
return this.items.filter(x => getEntityRef(x).getId() !== message.data.id);
|
|
65
|
+
}));
|
|
66
|
+
break;
|
|
67
|
+
}
|
|
68
|
+
;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
class MessageChannel {
|
|
72
|
+
constructor() {
|
|
73
|
+
this.listeners = [];
|
|
74
|
+
}
|
|
75
|
+
async handle(message) {
|
|
76
|
+
for (const l of this.listeners) {
|
|
77
|
+
l(message);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
export class LiveQueryClient {
|
|
82
|
+
constructor(lqp, provider) {
|
|
83
|
+
this.lqp = lqp;
|
|
84
|
+
this.provider = provider;
|
|
85
|
+
this.clientId = uuid();
|
|
86
|
+
this.queries = new Map();
|
|
87
|
+
this.channels = new Map();
|
|
88
|
+
}
|
|
89
|
+
runPromise(p) {
|
|
90
|
+
return p;
|
|
91
|
+
}
|
|
92
|
+
close() {
|
|
93
|
+
this.queries.clear();
|
|
94
|
+
this.channels.clear();
|
|
95
|
+
this.closeIfNoListeners();
|
|
96
|
+
}
|
|
97
|
+
subscribeChannel(key, onResult) {
|
|
98
|
+
let onUnsubscribe = () => { };
|
|
99
|
+
this.openIfNoOpened().then(() => {
|
|
100
|
+
let q = this.channels.get(key);
|
|
101
|
+
if (!q) {
|
|
102
|
+
this.channels.set(key, q = new MessageChannel());
|
|
103
|
+
this.provider.post(defaultRemult.apiClient.url + '/' + streamUrl, {
|
|
104
|
+
channel: key,
|
|
105
|
+
clientId: this.clientId,
|
|
106
|
+
remove: false
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
}
|
|
111
|
+
q.listeners.push(onResult);
|
|
112
|
+
onUnsubscribe = () => {
|
|
113
|
+
q.listeners.splice(q.listeners.indexOf(onResult), 1);
|
|
114
|
+
if (q.listeners.length == 0) {
|
|
115
|
+
this.channels.delete(key);
|
|
116
|
+
}
|
|
117
|
+
this.closeIfNoListeners();
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
return () => {
|
|
121
|
+
onUnsubscribe();
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
closeIfNoListeners() {
|
|
125
|
+
if (this.queries.size === 0 && this.channels.size === 0) {
|
|
126
|
+
this.runPromise(this.closeListener.then(x => x()));
|
|
127
|
+
this.closeListener = undefined;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
subscribe(repo, options, onResult) {
|
|
131
|
+
let alive = true;
|
|
132
|
+
let onUnsubscribe = () => { };
|
|
133
|
+
this.runPromise(this.openIfNoOpened().then(() => repo.buildEntityDataProviderFindOptions(options)
|
|
134
|
+
.then(opts => {
|
|
135
|
+
if (!alive)
|
|
136
|
+
return;
|
|
137
|
+
const { url, filterObject } = new RestEntityDataProvider(() => defaultRemult.apiClient.url + '/' + repo.metadata.key, () => this.provider, repo.metadata)
|
|
138
|
+
.buildFindRequest(opts);
|
|
139
|
+
const eventTypeKey = JSON.stringify({ url, filterObject });
|
|
140
|
+
let q = this.queries.get(eventTypeKey);
|
|
141
|
+
if (!q) {
|
|
142
|
+
this.queries.set(eventTypeKey, q = new LiveQueryOnFrontEnd(repo, { entityKey: repo.metadata.key, orderBy: options.orderBy }));
|
|
143
|
+
url.add("__action", 'subscribe|' + this.clientId);
|
|
144
|
+
const thenResult = (r) => {
|
|
145
|
+
this.runPromise(q.setAllItems(r.result));
|
|
146
|
+
q.id = r.id;
|
|
147
|
+
};
|
|
148
|
+
if (filterObject) {
|
|
149
|
+
this.runPromise(this.provider.post(url.url, filterObject).then(thenResult));
|
|
150
|
+
}
|
|
151
|
+
else
|
|
152
|
+
this.runPromise(this.provider.get(url.url).then(thenResult));
|
|
153
|
+
}
|
|
154
|
+
else {
|
|
155
|
+
onResult(x => q.items);
|
|
156
|
+
}
|
|
157
|
+
q.listeners.push(onResult);
|
|
158
|
+
onUnsubscribe = () => {
|
|
159
|
+
q.listeners.splice(q.listeners.indexOf(onResult), 1);
|
|
160
|
+
if (q.listeners.length == 0) {
|
|
161
|
+
this.queries.delete(eventTypeKey);
|
|
162
|
+
}
|
|
163
|
+
this.closeIfNoListeners();
|
|
164
|
+
};
|
|
165
|
+
})));
|
|
166
|
+
return () => {
|
|
167
|
+
alive = false;
|
|
168
|
+
onUnsubscribe();
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
openIfNoOpened() {
|
|
172
|
+
if (!this.provider) {
|
|
173
|
+
this.provider = buildRestDataProvider(defaultRemult.apiClient.httpClient);
|
|
174
|
+
}
|
|
175
|
+
if (!this.closeListener)
|
|
176
|
+
return this.runPromise(this.closeListener = this.lqp.openStreamAndReturnCloseFunction(this.clientId, message => {
|
|
177
|
+
for (const q of this.queries.values()) {
|
|
178
|
+
if (q.id === message.event) {
|
|
179
|
+
this.runPromise(q.handle(message.data));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
const channel = this.channels.get(message.event);
|
|
183
|
+
if (channel) {
|
|
184
|
+
channel.handle(message.data);
|
|
185
|
+
}
|
|
186
|
+
}));
|
|
187
|
+
return this.closeListener;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
export class AMessageChannel {
|
|
191
|
+
constructor(key, subscribedAllowed) {
|
|
192
|
+
this.subscribedAllowed = subscribedAllowed;
|
|
193
|
+
if (typeof key === "string")
|
|
194
|
+
this.key = () => key;
|
|
195
|
+
else
|
|
196
|
+
this.key = key;
|
|
197
|
+
}
|
|
198
|
+
userCanSubscribe(channel, remult) {
|
|
199
|
+
if (!remult)
|
|
200
|
+
remult = defaultRemult;
|
|
201
|
+
if (channel == this.key(remult) && remult.isAllowed(this.subscribedAllowed))
|
|
202
|
+
return true;
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
send(what, remult) {
|
|
206
|
+
if (!this.dispatcher)
|
|
207
|
+
throw new Error("Message couldn't be send since no dispatcher was set");
|
|
208
|
+
this.dispatcher.sendChannelMessage(this.key(remult || defaultRemult), what);
|
|
209
|
+
}
|
|
210
|
+
subscribe(client, onValue, remult) {
|
|
211
|
+
client.subscribeChannel(this.key(remult || defaultRemult), onValue);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
/*
|
|
215
|
+
[V] use entity normal http route for this - with __action.
|
|
216
|
+
[] move id from header to url in stream registration.
|
|
217
|
+
[] transaction accumulates messages.
|
|
218
|
+
[] on unsubscribe, also unsubscribe on server
|
|
219
|
+
[] consolidate channel & query
|
|
220
|
+
[] fix stream api url on server
|
|
221
|
+
[] remove client id from header
|
|
222
|
+
*/
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Remult } from '../..';
|
|
2
|
+
import { Repository, EntityRef, FindOptions } from '../remult3';
|
|
3
|
+
import { LiveQueryProvider } from '../data-api';
|
|
4
|
+
import { liveQueryMessage } from './LiveQuery';
|
|
5
|
+
export declare class LiveQueryManager implements LiveQueryProvider {
|
|
6
|
+
private dispatcher;
|
|
7
|
+
constructor(dispatcher: ServerEventDispatcher);
|
|
8
|
+
subscribe(repo: Repository<any>, clientId: string, findOptions: FindOptions<any>, remult: Remult, ids: any[]): string;
|
|
9
|
+
clients: clientInfo[];
|
|
10
|
+
sendMessage(key: string, message: liveQueryMessage): void;
|
|
11
|
+
hasListeners(ref: EntityRef<any>): boolean;
|
|
12
|
+
runPromise(p: Promise<any>): void;
|
|
13
|
+
saved(ref: EntityRef<any>): void;
|
|
14
|
+
deleted(ref: EntityRef<any>): void;
|
|
15
|
+
}
|
|
16
|
+
export interface clientInfo {
|
|
17
|
+
clientId: string;
|
|
18
|
+
queries: ({
|
|
19
|
+
id: string;
|
|
20
|
+
repo: Repository<any>;
|
|
21
|
+
findOptions: FindOptions<any>;
|
|
22
|
+
ids: any[];
|
|
23
|
+
})[];
|
|
24
|
+
}
|
|
25
|
+
export interface ServerEventDispatcher {
|
|
26
|
+
sendQueryMessage(message: ServerEventMessage): void;
|
|
27
|
+
sendChannelMessage<T>(channel: string, message: any): void;
|
|
28
|
+
}
|
|
29
|
+
export interface ServerEventMessage {
|
|
30
|
+
clientId: string;
|
|
31
|
+
queryId: string;
|
|
32
|
+
message: liveQueryMessage;
|
|
33
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { v4 as uuid } from 'uuid';
|
|
2
|
+
export class LiveQueryManager {
|
|
3
|
+
constructor(dispatcher) {
|
|
4
|
+
this.dispatcher = dispatcher;
|
|
5
|
+
this.clients = [];
|
|
6
|
+
}
|
|
7
|
+
subscribe(repo, clientId, findOptions, remult, ids) {
|
|
8
|
+
let client = this.clients.find(c => c.clientId === clientId);
|
|
9
|
+
if (!client) {
|
|
10
|
+
this.clients.push(client = { clientId: clientId, queries: [] });
|
|
11
|
+
}
|
|
12
|
+
const id = uuid();
|
|
13
|
+
console.log({ ids });
|
|
14
|
+
client.queries.push({
|
|
15
|
+
id,
|
|
16
|
+
findOptions: findOptions,
|
|
17
|
+
repo,
|
|
18
|
+
ids
|
|
19
|
+
});
|
|
20
|
+
return id;
|
|
21
|
+
}
|
|
22
|
+
sendMessage(key, message) {
|
|
23
|
+
for (const c of this.clients) {
|
|
24
|
+
for (const q of c.queries) {
|
|
25
|
+
if (q.repo.metadata.key === key) {
|
|
26
|
+
this.dispatcher.sendQueryMessage({ clientId: c.clientId, queryId: q.id, message });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
hasListeners(ref) {
|
|
32
|
+
for (const c of this.clients) {
|
|
33
|
+
for (const q of c.queries) {
|
|
34
|
+
if (q.repo.metadata.key === ref.metadata.key)
|
|
35
|
+
return true;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
runPromise(p) {
|
|
41
|
+
}
|
|
42
|
+
//TODO - reconsider usage of zone
|
|
43
|
+
saved(ref) {
|
|
44
|
+
const isNew = ref.isNew();
|
|
45
|
+
const origId = isNew ? ref.getId() : ref.getOriginalId();
|
|
46
|
+
for (const c of this.clients) {
|
|
47
|
+
for (const q of c.queries) {
|
|
48
|
+
if (q.repo.metadata.key === ref.metadata.key) {
|
|
49
|
+
this.runPromise(q.repo.find(q.findOptions).then(currentItems => {
|
|
50
|
+
const currentIds = currentItems.map(x => q.repo.getEntityRef(x).getId());
|
|
51
|
+
const sendMessage = (message) => {
|
|
52
|
+
console.log({ message: message.type });
|
|
53
|
+
this.dispatcher.sendQueryMessage({ clientId: c.clientId, queryId: q.id, message });
|
|
54
|
+
};
|
|
55
|
+
console.log({ ids: q.ids, currentIds });
|
|
56
|
+
for (const id of q.ids.filter(y => !currentIds.includes(y))) {
|
|
57
|
+
if (id != origId || !currentIds.includes(ref.getId()))
|
|
58
|
+
sendMessage({
|
|
59
|
+
type: "remove",
|
|
60
|
+
data: {
|
|
61
|
+
id: id
|
|
62
|
+
}
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
for (const item of currentItems) {
|
|
66
|
+
const itemRef = q.repo.getEntityRef(item);
|
|
67
|
+
if (itemRef.getId() == ref.getId() && q.ids.includes(origId)) {
|
|
68
|
+
sendMessage({
|
|
69
|
+
type: "replace",
|
|
70
|
+
data: {
|
|
71
|
+
oldId: origId,
|
|
72
|
+
item: itemRef.toApiJson()
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
else if (!q.ids.includes(itemRef.getId())) {
|
|
77
|
+
sendMessage({
|
|
78
|
+
type: "add",
|
|
79
|
+
data: { item: itemRef.toApiJson() }
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
q.ids = currentIds;
|
|
84
|
+
}));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
deleted(ref) {
|
|
90
|
+
if (!this.hasListeners(ref))
|
|
91
|
+
return;
|
|
92
|
+
this.sendMessage(ref.metadata.key, {
|
|
93
|
+
type: "remove",
|
|
94
|
+
data: { id: ref.getId() }
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|