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.
Files changed (39) hide show
  1. package/esm/remult-knex/index.js +9 -2
  2. package/esm/server/expressBridge.d.ts +2 -0
  3. package/esm/server/expressBridge.js +14 -8
  4. package/esm/server/initAsyncHooks.js +0 -1
  5. package/esm/src/backend-tests/backend-database-test-setup.backend-spec.d.ts +1 -0
  6. package/esm/src/backend-tests/backend-database-test-setup.backend-spec.js +10 -0
  7. package/esm/src/backend-tests/my-test.backend-spec.js +48 -0
  8. package/esm/src/live-query/EventSourceLiveQuery.d.ts +5 -0
  9. package/esm/src/live-query/EventSourceLiveQuery.js +48 -0
  10. package/esm/src/live-query/EventSourceLiveQueryProvider.d.ts +6 -0
  11. package/esm/src/live-query/EventSourceLiveQueryProvider.js +24 -0
  12. package/esm/src/live-query/LiveQuery.d.ts +67 -0
  13. package/esm/src/live-query/LiveQuery.js +222 -0
  14. package/esm/src/live-query/LiveQueryManager.d.ts +33 -0
  15. package/esm/src/live-query/LiveQueryManager.js +97 -0
  16. package/esm/src/remult-proxy.js +4 -0
  17. package/esm/src/shared-tests/db-tests-setup.d.ts +1 -1
  18. package/esm/src/shared-tests/db-tests-setup.js +1 -0
  19. package/esm/src/valueConverters.js +2 -0
  20. package/package.json +1 -1
  21. package/remult-knex/index.js +16 -8
  22. package/server/expressBridge.d.ts +2 -0
  23. package/server/expressBridge.js +14 -8
  24. package/server/initAsyncHooks.js +0 -1
  25. package/src/backend-tests/backend-database-test-setup.backend-spec.d.ts +1 -0
  26. package/src/backend-tests/backend-database-test-setup.backend-spec.js +11 -1
  27. package/src/backend-tests/my-test.backend-spec.js +57 -0
  28. package/src/live-query/EventSourceLiveQuery.d.ts +5 -0
  29. package/src/live-query/EventSourceLiveQuery.js +62 -0
  30. package/src/live-query/EventSourceLiveQueryProvider.d.ts +6 -0
  31. package/src/live-query/EventSourceLiveQueryProvider.js +32 -0
  32. package/src/live-query/LiveQuery.d.ts +67 -0
  33. package/src/live-query/LiveQuery.js +305 -0
  34. package/src/live-query/LiveQueryManager.d.ts +33 -0
  35. package/src/live-query/LiveQueryManager.js +196 -0
  36. package/src/remult-proxy.js +4 -0
  37. package/src/shared-tests/db-tests-setup.d.ts +1 -1
  38. package/src/shared-tests/db-tests-setup.js +1 -0
  39. package/src/valueConverters.js +2 -0
@@ -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 result = await insert.returning(this.entity.idMetadata.field.key);
157
- let newId = result[0].id;
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 found");
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;
@@ -2,7 +2,6 @@ import { AsyncLocalStorage } from 'async_hooks';
2
2
  import { RemultAsyncLocalStorage } from './expressBridge';
3
3
  let init = false;
4
4
  export function initAsyncHooks() {
5
- return;
6
5
  if (init)
7
6
  return;
8
7
  init = true;
@@ -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,5 @@
1
+ import { LiveQueryClient } from './LiveQuery';
2
+ export declare class EventSourceLiveQuery extends LiveQueryClient {
3
+ private wrapMessage?;
4
+ constructor(wrapMessage?: (what: () => void) => void);
5
+ }
@@ -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
+ }