jinaga-server 2.5.0 → 2.6.1

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 (59) hide show
  1. package/dist/authorization/authorization-keystore.d.ts +5 -4
  2. package/dist/authorization/authorization-keystore.js +16 -13
  3. package/dist/authorization/authorization-keystore.js.map +1 -1
  4. package/dist/http/router.d.ts +5 -3
  5. package/dist/http/router.js +125 -52
  6. package/dist/http/router.js.map +1 -1
  7. package/dist/http/stream.d.ts +10 -0
  8. package/dist/http/stream.js +35 -0
  9. package/dist/http/stream.js.map +1 -0
  10. package/dist/index.d.ts +1 -2
  11. package/dist/index.js +3 -3
  12. package/dist/index.js.map +1 -1
  13. package/dist/jinaga-server.js +7 -8
  14. package/dist/jinaga-server.js.map +1 -1
  15. package/dist/postgres/postgres-store.d.ts +4 -4
  16. package/dist/postgres/postgres-store.js +2 -2
  17. package/dist/postgres/postgres-store.js.map +1 -1
  18. package/dist/postgres/query-description.d.ts +75 -0
  19. package/dist/postgres/query-description.js +283 -0
  20. package/dist/postgres/query-description.js.map +1 -0
  21. package/dist/postgres/specification-result-sql.d.ts +2 -2
  22. package/dist/postgres/specification-result-sql.js +39 -324
  23. package/dist/postgres/specification-result-sql.js.map +1 -1
  24. package/dist/postgres/specification-sql.d.ts +2 -2
  25. package/dist/postgres/specification-sql.js +60 -163
  26. package/dist/postgres/specification-sql.js.map +1 -1
  27. package/http/sans-auth/.env.local +1 -0
  28. package/http/sans-auth/Create post sans auth.http +27 -0
  29. package/http/sans-auth/Create site sans auth.http +11 -0
  30. package/http/sans-auth/Feed sites.http +1 -0
  31. package/http/sans-auth/Feeds sites.http +12 -0
  32. package/http/sans-auth/Get posts sans auth.http +35 -0
  33. package/http/sans-auth/Get sites sans auth.http +12 -0
  34. package/http/sans-auth/stream.sh +2 -0
  35. package/integration-test/jinaga-test/Dockerfile +1 -1
  36. package/integration-test/jinaga-test/package-lock.json +917 -748
  37. package/integration-test/jinaga-test/package.json +3 -3
  38. package/package.json +2 -2
  39. package/replicator/package-lock.json +16 -16
  40. package/replicator/package.json +1 -1
  41. package/src/authorization/authorization-keystore.ts +23 -14
  42. package/src/http/router.ts +154 -64
  43. package/src/http/stream.ts +32 -0
  44. package/src/index.ts +1 -2
  45. package/src/jinaga-server.ts +8 -8
  46. package/src/postgres/postgres-store.ts +5 -8
  47. package/src/postgres/query-description.ts +435 -0
  48. package/src/postgres/specification-result-sql.ts +56 -489
  49. package/src/postgres/specification-sql.ts +71 -232
  50. package/test/authorization/authorizationSpec.ts +12 -2
  51. package/test/postgres/postgresSpecificationSpec.ts +15 -15
  52. package/dist/http/feed-cache.d.ts +0 -12
  53. package/dist/http/feed-cache.js +0 -3
  54. package/dist/http/feed-cache.js.map +0 -1
  55. package/dist/memory/memory-feed-cache.d.ts +0 -6
  56. package/dist/memory/memory-feed-cache.js +0 -17
  57. package/dist/memory/memory-feed-cache.js.map +0 -1
  58. package/src/http/feed-cache.ts +0 -14
  59. package/src/memory/memory-feed-cache.ts +0 -14
@@ -15,12 +15,12 @@
15
15
  "dependencies": {
16
16
  "@stablelib/base64": "^1.0.1",
17
17
  "express": "^4.17.2",
18
- "jest": "^28.1.0",
18
+ "jest": "^29.7.0",
19
19
  "jest-compact-reporter": "^1.2.9",
20
- "jinaga": "^5.4.0",
20
+ "jinaga": "^5.6.0",
21
21
  "node-forge": "^1.3.0",
22
22
  "passport": "^0.6.0",
23
- "pg": "^8.11.0",
23
+ "pg": "^8.11.1",
24
24
  "tweetnacl": "^1.0.3",
25
25
  "tweetnacl-util": "^0.15.1"
26
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "jinaga-server",
3
- "version": "2.5.0",
3
+ "version": "2.6.1",
4
4
  "description": "Data management for web and mobile applications.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -48,7 +48,7 @@
48
48
  },
49
49
  "dependencies": {
50
50
  "express": "^4.18.2",
51
- "jinaga": "^5.5.0",
51
+ "jinaga": "^5.6.0",
52
52
  "node-forge": "^1.3.1",
53
53
  "passport": "^0.6.0",
54
54
  "pg": "^8.11.0"
@@ -11,7 +11,7 @@
11
11
  "dependencies": {
12
12
  "cors": "^2.8.5",
13
13
  "express": "^4.18.1",
14
- "pg": "8.11"
14
+ "pg": "^8.11.3"
15
15
  },
16
16
  "devDependencies": {
17
17
  "@types/cors": "^2.8.12",
@@ -530,14 +530,14 @@
530
530
  "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ=="
531
531
  },
532
532
  "node_modules/pg": {
533
- "version": "8.11.0",
534
- "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.0.tgz",
535
- "integrity": "sha512-meLUVPn2TWgJyLmy7el3fQQVwft4gU5NGyvV0XbD41iU9Jbg8lCH4zexhIkihDzVHJStlt6r088G6/fWeNjhXA==",
533
+ "version": "8.11.3",
534
+ "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz",
535
+ "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==",
536
536
  "dependencies": {
537
537
  "buffer-writer": "2.0.0",
538
538
  "packet-reader": "1.0.0",
539
- "pg-connection-string": "^2.6.0",
540
- "pg-pool": "^3.6.0",
539
+ "pg-connection-string": "^2.6.2",
540
+ "pg-pool": "^3.6.1",
541
541
  "pg-protocol": "^1.6.0",
542
542
  "pg-types": "^2.1.0",
543
543
  "pgpass": "1.x"
@@ -546,7 +546,7 @@
546
546
  "node": ">= 8.0.0"
547
547
  },
548
548
  "optionalDependencies": {
549
- "pg-cloudflare": "^1.1.0"
549
+ "pg-cloudflare": "^1.1.1"
550
550
  },
551
551
  "peerDependencies": {
552
552
  "pg-native": ">=3.0.1"
@@ -558,15 +558,15 @@
558
558
  }
559
559
  },
560
560
  "node_modules/pg-cloudflare": {
561
- "version": "1.1.0",
562
- "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.0.tgz",
563
- "integrity": "sha512-tGM8/s6frwuAIyRcJ6nWcIvd3+3NmUKIs6OjviIm1HPPFEt5MzQDOTBQyhPWg/m0kCl95M6gA1JaIXtS8KovOA==",
561
+ "version": "1.1.1",
562
+ "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz",
563
+ "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==",
564
564
  "optional": true
565
565
  },
566
566
  "node_modules/pg-connection-string": {
567
- "version": "2.6.0",
568
- "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.0.tgz",
569
- "integrity": "sha512-x14ibktcwlHKoHxx9X3uTVW9zIGR41ZB6QNhHb21OPNdCCO3NaRnpJuwKIQSR4u+Yqjx4HCvy7Hh7VSy1U4dGg=="
567
+ "version": "2.6.2",
568
+ "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz",
569
+ "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA=="
570
570
  },
571
571
  "node_modules/pg-int8": {
572
572
  "version": "1.0.1",
@@ -577,9 +577,9 @@
577
577
  }
578
578
  },
579
579
  "node_modules/pg-pool": {
580
- "version": "3.6.0",
581
- "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.0.tgz",
582
- "integrity": "sha512-clFRf2ksqd+F497kWFyM21tMjeikn60oGDmqMT8UBrynEwVEX/5R5xd2sdvdo1cZCFlguORNpVuqxIj+aK4cfQ==",
580
+ "version": "3.6.1",
581
+ "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz",
582
+ "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==",
583
583
  "peerDependencies": {
584
584
  "pg": ">=8.0"
585
585
  }
@@ -17,7 +17,7 @@
17
17
  "dependencies": {
18
18
  "cors": "^2.8.5",
19
19
  "express": "^4.18.1",
20
- "pg": "8.11"
20
+ "pg": "8.11.3"
21
21
  },
22
22
  "devDependencies": {
23
23
  "@types/cors": "^2.8.12",
@@ -6,11 +6,12 @@ import {
6
6
  DistributionRules,
7
7
  FactEnvelope,
8
8
  FactFeed,
9
+ FactManager,
9
10
  FactRecord,
10
11
  FactReference,
11
- Feed,
12
12
  Forbidden,
13
13
  Query,
14
+ ReferencesByName,
14
15
  Specification,
15
16
  Storage,
16
17
  UserIdentity,
@@ -19,7 +20,6 @@ import {
19
20
  } from "jinaga";
20
21
 
21
22
  import { Keystore } from "../keystore";
22
- import { distinct } from "../util/fn";
23
23
  import { DistributedFactCache } from "./distributed-fact-cache";
24
24
 
25
25
  export class AuthorizationKeystore implements Authorization {
@@ -28,6 +28,7 @@ export class AuthorizationKeystore implements Authorization {
28
28
  private distributedFacts: DistributedFactCache = new DistributedFactCache();
29
29
 
30
30
  constructor(
31
+ private factManager: FactManager,
31
32
  private store: Storage,
32
33
  private keystore: Keystore,
33
34
  authorizationRules: AuthorizationRules | null,
@@ -52,7 +53,7 @@ export class AuthorizationKeystore implements Authorization {
52
53
  }
53
54
 
54
55
  query(userIdentity: UserIdentity | null, start: FactReference, query: Query) {
55
- return this.store.query(start, query);
56
+ return this.factManager.query(start, query);
56
57
  }
57
58
 
58
59
  async read(userIdentity: UserIdentity | null, start: FactReference[], specification: Specification) {
@@ -60,26 +61,34 @@ export class AuthorizationKeystore implements Authorization {
60
61
  const userReference: FactReference | null = userIdentity
61
62
  ? await this.keystore.getUserFact(userIdentity)
62
63
  : null;
64
+ const namedStart = specification.given.reduce((map, label, index) => ({
65
+ ...map,
66
+ [label.name]: start[index]
67
+ }), {} as ReferencesByName);
63
68
  // Break the specification into feeds and check distribution.
64
69
  const feeds = buildFeeds(specification);
65
- const canDistribute = await this.distributionEngine.canDistributeToAll(feeds, start, userReference);
70
+ const canDistribute = await this.distributionEngine.canDistributeToAll(feeds, namedStart, userReference);
66
71
  if (canDistribute.type === "failure") {
67
72
  throw new Forbidden(canDistribute.reason);
68
73
  }
69
74
  }
70
- return await this.store.read(start, specification);
75
+ return await this.factManager.read(start, specification);
71
76
  }
72
77
 
73
- async feed(userIdentity: UserIdentity | null, feed: Feed, start: FactReference[], bookmark: string): Promise<FactFeed> {
78
+ async feed(userIdentity: UserIdentity | null, specification: Specification, start: FactReference[], bookmark: string): Promise<FactFeed> {
74
79
  if (this.distributionEngine) {
75
80
  const userReference: FactReference | null = userIdentity
76
81
  ? await this.keystore.getUserFact(userIdentity)
77
82
  : null;
78
- const canDistribute = await this.distributionEngine.canDistributeToAll([feed], start, userReference);
83
+ const namedStart = specification.given.reduce((map, label, index) => ({
84
+ ...map,
85
+ [label.name]: start[index]
86
+ }), {} as ReferencesByName);
87
+ const canDistribute = await this.distributionEngine.canDistributeToAll([specification], namedStart, userReference);
79
88
  if (canDistribute.type === "failure") {
80
89
  throw new Forbidden(canDistribute.reason);
81
90
  }
82
- const factFeed = await this.store.feed(feed, start, bookmark);
91
+ const factFeed = await this.store.feed(specification, start, bookmark);
83
92
  const factReferences = factFeed.tuples
84
93
  .flatMap(tuple => tuple.facts)
85
94
  .filter((value, index, self) => self.findIndex(factReferenceEquals(value)) === index);
@@ -87,7 +96,7 @@ export class AuthorizationKeystore implements Authorization {
87
96
  return factFeed;
88
97
  }
89
98
  else {
90
- return await this.store.feed(feed, start, bookmark);
99
+ return await this.store.feed(specification, start, bookmark);
91
100
  }
92
101
  }
93
102
 
@@ -99,12 +108,12 @@ export class AuthorizationKeystore implements Authorization {
99
108
  throw new Forbidden("Unauthorized");
100
109
  }
101
110
  }
102
- return await this.store.load(references);
111
+ return await this.factManager.load(references);
103
112
  }
104
113
 
105
114
  async save(userIdentity: UserIdentity | null, facts: FactRecord[]) {
106
115
  if (!this.authorizationEngine) {
107
- const envelopes = await this.store.save(facts.map(fact => ({
116
+ const envelopes = await this.factManager.save(facts.map(fact => ({
108
117
  fact,
109
118
  signatures: []
110
119
  })));
@@ -115,7 +124,7 @@ export class AuthorizationKeystore implements Authorization {
115
124
  const authorizedFacts = await this.authorizationEngine.authorizeFacts(facts, userFact);
116
125
  if (userIdentity) {
117
126
  const signedFacts = await this.keystore.signFacts(userIdentity, authorizedFacts);
118
- const envelopes = await this.store.save(signedFacts);
127
+ const envelopes = await this.factManager.save(signedFacts);
119
128
  return envelopes.map(envelope => envelope.fact);
120
129
  }
121
130
  else {
@@ -123,7 +132,7 @@ export class AuthorizationKeystore implements Authorization {
123
132
  }
124
133
  }
125
134
 
126
- async verifyDistribution(userIdentity: UserIdentity | null, feeds: Feed[], start: FactReference[]): Promise<void> {
135
+ async verifyDistribution(userIdentity: UserIdentity | null, feeds: Specification[], namedStart: ReferencesByName): Promise<void> {
127
136
  if (!this.distributionEngine) {
128
137
  return;
129
138
  }
@@ -131,7 +140,7 @@ export class AuthorizationKeystore implements Authorization {
131
140
  const userReference: FactReference | null = userIdentity
132
141
  ? await this.keystore.getUserFact(userIdentity)
133
142
  : null;
134
- const canDistribute = await this.distributionEngine.canDistributeToAll(feeds, start, userReference);
143
+ const canDistribute = await this.distributionEngine.canDistributeToAll(feeds, namedStart, userReference);
135
144
  if (canDistribute.type === "failure") {
136
145
  throw new Forbidden(canDistribute.reason);
137
146
  }
@@ -2,9 +2,11 @@ import { Handler, Router } from "express";
2
2
  import {
3
3
  Authorization,
4
4
  Declaration,
5
+ FactManager,
5
6
  FactRecord,
6
7
  FactReference,
7
- Feed,
8
+ FeedCache,
9
+ FeedObject,
8
10
  FeedResponse,
9
11
  FeedsResponse,
10
12
  Forbidden,
@@ -14,6 +16,7 @@ import {
14
16
  ProjectedResult,
15
17
  QueryMessage,
16
18
  QueryResponse,
19
+ ReferencesByName,
17
20
  SaveMessage,
18
21
  Specification,
19
22
  SpecificationParser,
@@ -21,42 +24,91 @@ import {
21
24
  UserIdentity,
22
25
  buildFeeds,
23
26
  computeObjectHash,
27
+ computeTupleSubsetHash,
24
28
  fromDescriptiveString,
29
+ invertSpecification,
25
30
  parseLoadMessage,
26
31
  parseQueryMessage,
27
- parseSaveMessage,
32
+ parseSaveMessage
28
33
  } from "jinaga";
29
34
 
30
- import { FeedCache, FeedDefinition } from "./feed-cache";
35
+ import { Stream } from "./stream";
31
36
 
32
37
  interface ParsedQs { [key: string]: undefined | string | string[] | ParsedQs | ParsedQs[] }
33
38
 
34
- function get<U>(method: ((req: RequestUser, params: { [key: string]: string }, query: ParsedQs) => Promise<U>)): Handler {
39
+ function getOrStream<U>(
40
+ getMethod: ((req: RequestUser, params: { [key: string]: string }, query: ParsedQs) => Promise<U | null>),
41
+ streamMethod: ((req: RequestUser, params: { [key: string]: string }, query: ParsedQs) => Promise<Stream<U> | null>)
42
+ ): Handler {
35
43
  return (req, res, next) => {
36
44
  const user = <RequestUser>req.user;
37
- method(user, req.params, req.query)
38
- .then(response => {
39
- if (!response) {
40
- res.sendStatus(404);
45
+ const accept = req.headers["accept"];
46
+ if (accept && accept.indexOf("application/x-jinaga-feed-stream") >= 0) {
47
+ streamMethod(user, req.params, req.query)
48
+ .then(response => {
49
+ if (!response) {
50
+ res.sendStatus(404);
51
+ next();
52
+ }
53
+ else {
54
+ res.type("application/x-jinaga-feed-stream");
55
+ res.set("Connection", "keep-alive");
56
+ res.set("Cache-Control", "no-cache");
57
+ res.set("Access-Control-Allow-Origin", "*");
58
+ res.flushHeaders();
59
+ req.on("close", () => {
60
+ response.close();
61
+ });
62
+ const timeout = setTimeout(() => {
63
+ response.close();
64
+ }, 5 * 60 * 1000);
65
+ response
66
+ .next(data => {
67
+ res.write(JSON.stringify(data) + "\n\n");
68
+ })
69
+ .done(() => {
70
+ clearTimeout(timeout);
71
+ next();
72
+ });
73
+ }
74
+ })
75
+ .catch(error => {
76
+ if (error instanceof Forbidden) {
77
+ res.type("text");
78
+ res.status(403).send(error.message);
79
+ }
80
+ else {
81
+ Trace.error(error);
82
+ res.status(500).send(error.message);
83
+ }
41
84
  next();
42
- }
43
- else {
44
- res.type("json");
45
- res.send(JSON.stringify(response));
85
+ });
86
+ }
87
+ else {
88
+ getMethod(user, req.params, req.query)
89
+ .then(response => {
90
+ if (!response) {
91
+ res.sendStatus(404);
92
+ next();
93
+ }
94
+ else {
95
+ res.type("json");
96
+ res.send(JSON.stringify(response));
97
+ next();
98
+ }
99
+ })
100
+ .catch(error => {
101
+ if (error instanceof Forbidden) {
102
+ res.type("text");
103
+ res.status(403).send(error.message);
104
+ }
105
+ else {
106
+ Trace.error(error);
107
+ res.status(500).send(error.message);
108
+ }
46
109
  next();
47
- }
48
- })
49
- .catch(error => {
50
- if (error instanceof Forbidden) {
51
- res.type("text");
52
- res.status(403).send(error.message);
53
- }
54
- else {
55
- Trace.error(error);
56
- res.status(500).send(error.message);
57
- }
58
- next();
59
- });
110
+ });
111
+ }
60
112
  };
61
113
  }
62
114
 
@@ -124,7 +176,7 @@ function postString<U>(method: (user: RequestUser, message: string) => Promise<U
124
176
  return (req, res, next) => {
125
177
  const user = <RequestUser>req.user;
126
178
  const input = parseString(req.body);
127
- if (!input || typeof(input) !== 'string') {
179
+ if (!input || typeof (input) !== 'string') {
128
180
  res.type("text");
129
181
  res.status(500).send('Expected Content-Type text/plain. Ensure that you have called app.use(express.text()).');
130
182
  }
@@ -187,7 +239,7 @@ function postStringCreate(method: (user: RequestUser, message: string) => Promis
187
239
  return (req, res, next) => {
188
240
  const user = <RequestUser>req.user;
189
241
  const input = parseString(req.body);
190
- if (!input || typeof(input) !== 'string') {
242
+ if (!input || typeof (input) !== 'string') {
191
243
  res.type("text");
192
244
  res.status(500).send('Expected Content-Type text/plain. Ensure that you have called app.use(express.text()).');
193
245
  }
@@ -214,7 +266,7 @@ function postStringCreate(method: (user: RequestUser, message: string) => Promis
214
266
  };
215
267
  }
216
268
 
217
- function serializeUserIdentity(user: RequestUser | null) : UserIdentity | null {
269
+ function serializeUserIdentity(user: RequestUser | null): UserIdentity | null {
218
270
  if (!user) {
219
271
  return null;
220
272
  }
@@ -233,7 +285,7 @@ export interface RequestUser {
233
285
  export class HttpRouter {
234
286
  handler: Handler;
235
287
 
236
- constructor(private authorization: Authorization, private feedCache: FeedCache, backwardCompatible: boolean) {
288
+ constructor(private factManager: FactManager, private authorization: Authorization, private feedCache: FeedCache, backwardCompatible: boolean) {
237
289
  const router = Router();
238
290
  router.get('/login', getAuthenticate(user => this.login(user)));
239
291
  if (backwardCompatible) {
@@ -257,7 +309,9 @@ export class HttpRouter {
257
309
  parseString,
258
310
  (user, input: string) => this.feeds(user, input)
259
311
  ));
260
- router.get('/feeds/:hash', get((user, params, query) => this.feed(user, params, query)));
312
+ router.get('/feeds/:hash', getOrStream<FeedResponse>(
313
+ (user, params, query) => this.feed(user, params, query),
314
+ (user, params, query) => this.streamFeed(user, params, query)));
261
315
 
262
316
  this.handler = router;
263
317
  }
@@ -273,7 +327,7 @@ export class HttpRouter {
273
327
  };
274
328
  }
275
329
 
276
- private async query(user: RequestUser | null, queryMessage: QueryMessage) : Promise<QueryResponse> {
330
+ private async query(user: RequestUser | null, queryMessage: QueryMessage): Promise<QueryResponse> {
277
331
  const userIdentity = serializeUserIdentity(user);
278
332
  const query = fromDescriptiveString(queryMessage.query);
279
333
  const result = await this.authorization.query(userIdentity, queryMessage.start, query);
@@ -282,7 +336,7 @@ export class HttpRouter {
282
336
  };
283
337
  }
284
338
 
285
- private async load(user: RequestUser, loadMessage: LoadMessage) : Promise<LoadResponse> {
339
+ private async load(user: RequestUser, loadMessage: LoadMessage): Promise<LoadResponse> {
286
340
  const userIdentity = serializeUserIdentity(user);
287
341
  const result = await this.authorization.load(userIdentity, loadMessage.references);
288
342
  return {
@@ -290,7 +344,7 @@ export class HttpRouter {
290
344
  };
291
345
  }
292
346
 
293
- private async save(user: RequestUser | null, saveMessage: SaveMessage) : Promise<void> {
347
+ private async save(user: RequestUser | null, saveMessage: SaveMessage): Promise<void> {
294
348
  const userIdentity = serializeUserIdentity(user);
295
349
  await this.authorization.save(userIdentity, saveMessage.facts);
296
350
  }
@@ -346,33 +400,21 @@ export class HttpRouter {
346
400
  throw new Error(`The type of start fact ${i} (${start[i].type}) does not match the type of input ${i} (${specification.given[i].type})`);
347
401
  }
348
402
  }
403
+
404
+ const namedStart = specification.given.reduce((map, label, index) => ({
405
+ ...map,
406
+ [label.name]: start[index]
407
+ }), {} as ReferencesByName);
349
408
 
350
409
  // Verify that I can distribute all feeds to the user.
351
410
  const feeds = buildFeeds(specification);
352
411
  const userIdentity = serializeUserIdentity(user);
353
- await this.authorization.verifyDistribution(userIdentity, feeds, start);
354
-
355
- const feedDefinitionsByHash = feeds.map(feed => {
356
- const indexedStart = feed.inputs.map(input => ({
357
- factReference: start[input.inputIndex],
358
- index: input.inputIndex
359
- }));
360
- const feedDefinition: FeedDefinition = {
361
- start: indexedStart,
362
- feed
363
- };
364
- return {
365
- hash: urlSafeHash(feed),
366
- feedDefinition
367
- };
368
- });
369
- // Store all feeds in the cache.
370
- for (const d of feedDefinitionsByHash) {
371
- await this.feedCache.storeFeed(d.hash, d.feedDefinition);
372
- }
412
+ await this.authorization.verifyDistribution(userIdentity, feeds, namedStart);
413
+
414
+ const feedHashes = this.feedCache.addFeeds(feeds, namedStart);
373
415
 
374
416
  return {
375
- feeds: feedDefinitionsByHash.map(f => f.hash)
417
+ feeds: feedHashes
376
418
  }
377
419
  }
378
420
 
@@ -390,10 +432,7 @@ export class HttpRouter {
390
432
  const bookmark = query["b"] as string ?? "";
391
433
 
392
434
  const userIdentity = serializeUserIdentity(user);
393
- const start = feedDefinition.start.reduce((start, input) => {
394
- start[input.index] = input.factReference;
395
- return start;
396
- }, [] as FactReference[]);
435
+ const start = feedDefinition.feed.given.map(label => feedDefinition.namedStart[label.name]);
397
436
  const results = await this.authorization.feed(userIdentity, feedDefinition.feed, start, bookmark);
398
437
  // Return distinct fact references from all the tuples.
399
438
  const references = results.tuples.flatMap(t => t.facts).filter((value, index, self) =>
@@ -406,6 +445,62 @@ export class HttpRouter {
406
445
  return response;
407
446
  }
408
447
 
448
+ private async streamFeed(user: RequestUser | null, params: { [key: string]: string }, query: ParsedQs): Promise<Stream<FeedResponse> | null> {
449
+ const feedHash = params["hash"];
450
+ if (!feedHash) {
451
+ return null;
452
+ }
453
+
454
+ const feedDefinition = await this.feedCache.getFeed(feedHash);
455
+ if (!feedDefinition) {
456
+ return null;
457
+ }
458
+
459
+ let bookmark = query["b"] as string ?? "";
460
+
461
+ const userIdentity = serializeUserIdentity(user);
462
+ const start = feedDefinition.feed.given.map(label => feedDefinition.namedStart[label.name]);
463
+ const givenHash = computeObjectHash(feedDefinition.namedStart);
464
+
465
+ const stream = new Stream<FeedResponse>();
466
+ Trace.info("Initial response");
467
+ bookmark = await this.streamFeedResponse(userIdentity, feedDefinition, start, bookmark, stream);
468
+ const inverses = invertSpecification(feedDefinition.feed);
469
+ const listeners = inverses.map(inverse => this.factManager.addSpecificationListener(
470
+ inverse.inverseSpecification,
471
+ async (results) => {
472
+ // Filter out results that do not match the given.
473
+ const matchingResults = results.filter(pr =>
474
+ givenHash === computeTupleSubsetHash(pr.tuple, inverse.givenSubset));
475
+ if (matchingResults.length != 0) {
476
+ bookmark = await this.streamFeedResponse(userIdentity, feedDefinition, start, bookmark, stream, true);
477
+ }
478
+ }
479
+ ));
480
+ stream.done(() => {
481
+ Trace.info("Done");
482
+ for (const listener of listeners) {
483
+ this.factManager.removeSpecificationListener(listener);
484
+ }
485
+ });
486
+ return stream;
487
+ }
488
+
489
+ private async streamFeedResponse(userIdentity: UserIdentity | null, feedDefinition: FeedObject, start: FactReference[], bookmark: string, stream: Stream<FeedResponse>, skipIfEmpty = false): Promise<string> {
490
+ const results = await this.authorization.feed(userIdentity, feedDefinition.feed, start, bookmark);
491
+ // Return distinct fact references from all the tuples.
492
+ const references = results.tuples.flatMap(t => t.facts).filter((value, index, self) => self.findIndex(f => f.hash === value.hash && f.type === value.type) === index
493
+ );
494
+ if (!skipIfEmpty || references.length > 0) {
495
+ const response: FeedResponse = {
496
+ references,
497
+ bookmark: results.bookmark
498
+ };
499
+ stream.feed(response);
500
+ }
501
+ return results.bookmark;
502
+ }
503
+
409
504
  private async getKnownFacts(user: RequestUser | null): Promise<Declaration> {
410
505
  if (user) {
411
506
  const userFact = await this.authorization.getOrCreateUserFact({
@@ -430,7 +525,7 @@ export class HttpRouter {
430
525
  }
431
526
  }
432
527
 
433
- private selectStart(specification: Specification, declaration: Declaration) : FactReference[] {
528
+ private selectStart(specification: Specification, declaration: Declaration): FactReference[] {
434
529
  // Select starting facts that match the inputs
435
530
  return specification.given.map(input => {
436
531
  const declaredFact = declaration.find(d => d.name === input.name);
@@ -449,11 +544,6 @@ function parseString(input: any): string {
449
544
  return input;
450
545
  }
451
546
 
452
- function urlSafeHash(feed: Feed): string {
453
- const base64 = computeObjectHash(feed);
454
- return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
455
- }
456
-
457
547
  function extractResults(obj: any): any {
458
548
  if (Array.isArray(obj)) {
459
549
  const projectedResults: ProjectedResult[] = obj;
@@ -0,0 +1,32 @@
1
+ export class Stream<T> {
2
+ private queue: T[] = [];
3
+ private handlers: ((data: T) => void)[] = [];
4
+ private doneHandlers: (() => void)[] = [];
5
+ private closed = false;
6
+
7
+ next(handler: (data: T) => void): Stream<T> {
8
+ this.handlers.push(handler);
9
+ this.queue.forEach(data => handler(data));
10
+ return this;
11
+ }
12
+
13
+ done(handler: () => void): Stream<T> {
14
+ this.doneHandlers.push(handler);
15
+ return this;
16
+ }
17
+
18
+ close(): void {
19
+ this.closed = true;
20
+ this.doneHandlers.forEach(handler => handler());
21
+ this.queue = [];
22
+ this.handlers = [];
23
+ this.doneHandlers = [];
24
+ }
25
+
26
+ feed(data: any): void {
27
+ if (!this.closed) {
28
+ this.queue.push(data);
29
+ this.handlers.forEach(handler => handler(data));
30
+ }
31
+ }
32
+ }
package/src/index.ts CHANGED
@@ -2,11 +2,10 @@ export { AuthenticationDevice } from "./authentication/authentication-device";
2
2
  export { AuthenticationSession } from "./authentication/authentication-session";
3
3
  export { AuthorizationKeystore } from "./authorization/authorization-keystore";
4
4
  export { DistributedFactCache } from "./authorization/distributed-fact-cache";
5
- export { FeedCache, FeedDefinition } from "./http/feed-cache";
6
5
  export { HttpRouter, RequestUser } from "./http/router";
6
+ export { Stream } from "./http/stream";
7
7
  export { JinagaServer, JinagaServerConfig, JinagaServerInstance, tracePool } from "./jinaga-server";
8
8
  export { Keystore } from "./keystore";
9
- export { MemoryFeedCache } from "./memory/memory-feed-cache";
10
9
  export { MemoryKeystore } from "./memory/memory-keystore";
11
10
  export { PostgresKeystore } from "./postgres/postgres-keystore";
12
11
  export { PostgresStore } from "./postgres/postgres-store";