jinaga-server 3.5.5 → 3.7.0

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 (50) hide show
  1. package/dist/authorization/authorization-keystore.d.ts +34 -2
  2. package/dist/authorization/authorization-keystore.js +59 -4
  3. package/dist/authorization/authorization-keystore.js.map +1 -1
  4. package/dist/feeds/feed-stream-session.d.ts +84 -0
  5. package/dist/feeds/feed-stream-session.js +331 -0
  6. package/dist/feeds/feed-stream-session.js.map +1 -0
  7. package/dist/http/router.d.ts +10 -3
  8. package/dist/http/router.js +186 -184
  9. package/dist/http/router.js.map +1 -1
  10. package/dist/index.d.ts +2 -1
  11. package/dist/index.js +5 -1
  12. package/dist/index.js.map +1 -1
  13. package/dist/jinaga-server.d.ts +7 -0
  14. package/dist/jinaga-server.js +19 -2
  15. package/dist/jinaga-server.js.map +1 -1
  16. package/dist/memory/memory-keystore.js +1 -1
  17. package/dist/memory/memory-keystore.js.map +1 -1
  18. package/dist/postgres/query-description.d.ts +1 -0
  19. package/dist/postgres/query-description.js +34 -3
  20. package/dist/postgres/query-description.js.map +1 -1
  21. package/dist/postgres/specification-sql.js +7 -1
  22. package/dist/postgres/specification-sql.js.map +1 -1
  23. package/docs/design/feeds-persistent-proxy-plan.md +11 -1
  24. package/integration-test/jinaga-test/distribution-multi-given.test.js +184 -0
  25. package/integration-test/jinaga-test/distribution-negating-feed.test.js +202 -0
  26. package/integration-test/jinaga-test/feed-existential-input.test.js +241 -0
  27. package/integration-test/jinaga-test/feeds-endpoint.test.js +140 -0
  28. package/integration-test/jinaga-test/package-lock.json +4 -4
  29. package/integration-test/jinaga-test/package.json +3 -2
  30. package/integration-test/jinaga-test/subscription-anchor-arrival.test.js +129 -0
  31. package/integration-test/jinaga-test/subscription-helpers.js +60 -0
  32. package/integration-test/jinaga-test/subscription-late-auth.test.js +245 -0
  33. package/integration-test/jinaga-test/subscription-no-rule.test.js +137 -0
  34. package/package.json +2 -2
  35. package/src/authorization/authorization-keystore.ts +106 -5
  36. package/src/feeds/feed-stream-session.ts +386 -0
  37. package/src/http/router.ts +212 -237
  38. package/src/index.ts +2 -1
  39. package/src/jinaga-server.ts +45 -5
  40. package/src/memory/memory-keystore.ts +1 -1
  41. package/src/postgres/query-description.ts +66 -11
  42. package/src/postgres/specification-sql.ts +7 -1
  43. package/test/authorization/intermediateProjectionFeedSpec.ts +189 -0
  44. package/test/authorization/subscriptionIntersectionSpec.ts +220 -0
  45. package/test/feeds/feedStreamSessionRegressionSpec.ts +227 -0
  46. package/test/feeds/feedStreamSessionSpec.ts +351 -0
  47. package/test/http/anchorListenerSpec.ts +117 -0
  48. package/test/http/feedStreamSessionIntegrationSpec.ts +202 -0
  49. package/test/http/lateAuthRecoverySpec.ts +435 -0
  50. package/test/http/streamFeedAnchorSpec.ts +171 -0
@@ -0,0 +1,171 @@
1
+ import {
2
+ AuthorizationRules,
3
+ buildModel,
4
+ dehydrateFact,
5
+ FactManager,
6
+ FeedCache,
7
+ FeedResponse,
8
+ MemoryStore,
9
+ NetworkNoOp,
10
+ ObservableSource,
11
+ PassThroughFork,
12
+ User
13
+ } from "jinaga";
14
+
15
+ import { AuthorizationKeystore } from "../../src/authorization/authorization-keystore";
16
+ import { HttpRouter, RequestUser } from "../../src/http/router";
17
+ import { Stream } from "../../src/http/stream";
18
+ import { MemoryKeystore } from "../../src/memory/memory-keystore";
19
+
20
+ class Post {
21
+ public static Type = "stream.Post" as const;
22
+ public type = Post.Type;
23
+ constructor(public author: User, public body: string) { }
24
+ }
25
+
26
+ const model = buildModel(b => b
27
+ .type(User)
28
+ .type(Post, m => m.predecessor("author", User))
29
+ );
30
+
31
+ const userIdentity = { provider: "mock", id: "subscriber" };
32
+
33
+ interface Harness {
34
+ router: HttpRouter;
35
+ factManager: FactManager;
36
+ storage: MemoryStore;
37
+ requestUser: RequestUser;
38
+ author: User;
39
+ authorRef: { type: string; hash: string };
40
+ }
41
+
42
+ async function makeHarness(): Promise<Harness> {
43
+ const storage = new MemoryStore();
44
+ const keystore = new MemoryKeystore();
45
+ const userFact = await keystore.getOrCreateUserFact(userIdentity);
46
+ const author = new User(userFact.fields.publicKey);
47
+ const authorRef = dehydrateFact(author)[0];
48
+
49
+ const fork = new PassThroughFork(storage);
50
+ const observable = new ObservableSource(storage);
51
+ const network = new NetworkNoOp();
52
+ const factManager = new FactManager(fork, observable, storage, network, []);
53
+
54
+ const authorizationRules = new AuthorizationRules(model)
55
+ .any(User)
56
+ .any(Post);
57
+ const authorization = new AuthorizationKeystore(
58
+ factManager, storage, keystore, authorizationRules, null);
59
+ const feedCache = new FeedCache();
60
+
61
+ const router = new HttpRouter(factManager, authorization, feedCache, "*");
62
+
63
+ return {
64
+ router,
65
+ factManager,
66
+ storage,
67
+ requestUser: { provider: userIdentity.provider, id: userIdentity.id, profile: {} as any },
68
+ author,
69
+ authorRef
70
+ };
71
+ }
72
+
73
+ async function waitForMicrotasks(): Promise<void> {
74
+ for (let i = 0; i < 5; i++) {
75
+ await new Promise<void>(resolve => setImmediate(resolve));
76
+ }
77
+ }
78
+
79
+ describe("HttpRouter streamFeed anchor handling", () => {
80
+ it("delivers a post that joins to an anchor whose User fact arrives after subscribe", async () => {
81
+ const h = await makeHarness();
82
+
83
+ // Subscribe for posts by the author. The author fact's hash is
84
+ // referenced but not yet stored.
85
+ const input =
86
+ `let p: Jinaga.User = #${h.authorRef.hash}\n` +
87
+ `(p: Jinaga.User) {\n` +
88
+ ` post: stream.Post [\n` +
89
+ ` post->author: Jinaga.User = p\n` +
90
+ ` ]\n` +
91
+ `} => post`;
92
+
93
+ const feedsResponse = await (h.router as any).feeds(h.requestUser, input);
94
+ expect(feedsResponse.feeds.length).toBeGreaterThan(0);
95
+ const feedHash: string = feedsResponse.feeds[0];
96
+
97
+ const stream: Stream<FeedResponse> = await (h.router as any).streamFeed(
98
+ h.requestUser, { hash: feedHash }, {});
99
+ expect(stream).not.toBeNull();
100
+
101
+ const received: FeedResponse[] = [];
102
+ stream.next(r => { received.push(r); });
103
+
104
+ await waitForMicrotasks();
105
+ // The session catches up immediately (no tuples, empty waitlist) and
106
+ // emits a SYNC frame with empty references. No post has arrived yet.
107
+ expect(received.flatMap(r => r.references)).toEqual([]);
108
+
109
+ // Save the author + a post that joins to it.
110
+ const post = new Post(h.author, "first");
111
+ const envelopes = dehydrateFact(post).map(f => ({ fact: f, signatures: [] }));
112
+ await h.factManager.save(envelopes);
113
+
114
+ await waitForMicrotasks();
115
+ stream.close();
116
+
117
+ const allReferences = received.flatMap(r => r.references);
118
+ const postArrived = allReferences.some(r => r.type === Post.Type);
119
+ expect(postArrived).toBe(true);
120
+ });
121
+
122
+ it("re-runs the query when the anchor User fact arrives with descendants already in store", async () => {
123
+ const h = await makeHarness();
124
+
125
+ // Insert the post and its author directly through the underlying
126
+ // store so no observable notification fires for these. After
127
+ // subscribe, only the anchor-arrival path should trigger delivery.
128
+ const post = new Post(h.author, "preexisting");
129
+ const postFacts = dehydrateFact(post);
130
+ await h.storage.save(postFacts.map(f => ({ fact: f, signatures: [] })));
131
+
132
+ // Now subscribe.
133
+ const input =
134
+ `let p: Jinaga.User = #${h.authorRef.hash}\n` +
135
+ `(p: Jinaga.User) {\n` +
136
+ ` post: stream.Post [\n` +
137
+ ` post->author: Jinaga.User = p\n` +
138
+ ` ]\n` +
139
+ `} => post`;
140
+
141
+ const feedsResponse = await (h.router as any).feeds(h.requestUser, input);
142
+ const feedHash: string = feedsResponse.feeds[0];
143
+ const stream: Stream<FeedResponse> = await (h.router as any).streamFeed(
144
+ h.requestUser, { hash: feedHash }, {});
145
+
146
+ const received: FeedResponse[] = [];
147
+ stream.next(r => { received.push(r); });
148
+ await waitForMicrotasks();
149
+
150
+ // The initial query should already have included the post (since
151
+ // it's in the store), so the subscription is "activated" via the
152
+ // initial fetch. Record what we have.
153
+ const beforeCount = received.flatMap(r => r.references)
154
+ .filter(r => r.type === Post.Type).length;
155
+
156
+ // Re-save the author through the FactManager so the anchor
157
+ // listener fires. The dedup at the store level returns no new
158
+ // envelopes, but the listener still has to behave gracefully.
159
+ await h.factManager.save(postFacts
160
+ .filter(f => f.type === "Jinaga.User")
161
+ .map(f => ({ fact: f, signatures: [] })));
162
+ await waitForMicrotasks();
163
+ stream.close();
164
+
165
+ // At minimum the post is visible after the anchor save and the
166
+ // stream does not error. With the initial fetch the post was
167
+ // already delivered; this guards against regressions where the
168
+ // anchor path tears down the stream.
169
+ expect(beforeCount).toBeGreaterThanOrEqual(1);
170
+ });
171
+ });