tangled
alpha
login
or
join now
chadtmiller.com
/
slices-teal-relay
16
fork
atom
Demo using Slices Network GraphQL Relay API to make a teal.fm client
16
fork
atom
overview
issues
pulls
pipelines
add plays subscription
chadtmiller.com
5 months ago
0d1ebcce
0c418d16
+393
-55
8 changed files
expand all
collapse all
unified
split
deno.lock
package.json
schema.graphql
src
App.tsx
__generated__
AppSubscription.graphql.ts
TopAlbumsQuery.graphql.ts
TopTracksQuery.graphql.ts
main.tsx
+8
deno.lock
···
15
15
"npm:eslint-plugin-react-refresh@~0.4.22": "0.4.23_eslint@9.36.0",
16
16
"npm:eslint@^9.36.0": "9.36.0",
17
17
"npm:globals@^16.4.0": "16.4.0",
18
18
+
"npm:graphql-ws@^6.0.6": "6.0.6_graphql@16.11.0",
18
19
"npm:graphql@^16.11.0": "16.11.0",
19
20
"npm:postcss@^8.5.6": "8.5.6",
20
21
"npm:react-dom@^19.1.1": "19.2.0_react@19.2.0",
···
1324
1325
"graphemer@1.4.0": {
1325
1326
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
1326
1327
},
1328
1328
+
"graphql-ws@6.0.6_graphql@16.11.0": {
1329
1329
+
"integrity": "sha512-zgfER9s+ftkGKUZgc0xbx8T7/HMO4AV5/YuYiFc+AtgcO5T0v8AxYYNQ+ltzuzDZgNkYJaFspm5MMYLjQzrkmw==",
1330
1330
+
"dependencies": [
1331
1331
+
"graphql@16.11.0"
1332
1332
+
]
1333
1333
+
},
1327
1334
"graphql@15.3.0": {
1328
1335
"integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w=="
1329
1336
},
···
2012
2019
"npm:eslint-plugin-react-refresh@~0.4.22",
2013
2020
"npm:eslint@^9.36.0",
2014
2021
"npm:globals@^16.4.0",
2022
2022
+
"npm:graphql-ws@^6.0.6",
2015
2023
"npm:graphql@^16.11.0",
2016
2024
"npm:postcss@^8.5.6",
2017
2025
"npm:react-dom@^19.1.1",
+1
package.json
···
12
12
"schema:prod": "npx get-graphql-schema 'https://api.slices.network/graphql?slice=at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a' > schema.graphql"
13
13
},
14
14
"dependencies": {
15
15
+
"graphql-ws": "^6.0.6",
15
16
"react": "^19.1.1",
16
17
"react-dom": "^19.1.1",
17
18
"react-relay": "^20.1.1",
+89
-43
schema.graphql
···
40
40
}
41
41
42
42
type AppBskyActorProfileAggregated {
43
43
-
avatar: String
44
44
-
banner: String
45
45
-
createdAt: String
46
46
-
description: String
47
47
-
displayName: String
48
48
-
joinedViaStarterPack: String
49
49
-
labels: String
50
50
-
pinnedPost: String
43
43
+
avatar: JSON
44
44
+
banner: JSON
45
45
+
createdAt: JSON
46
46
+
description: JSON
47
47
+
displayName: JSON
48
48
+
joinedViaStarterPack: JSON
49
49
+
labels: JSON
50
50
+
pinnedPost: JSON
51
51
count: Int!
52
52
}
53
53
···
81
81
}
82
82
83
83
type AppBskyEmbedExternalAggregated {
84
84
-
external: String
84
84
+
external: JSON
85
85
count: Int!
86
86
}
87
87
···
115
115
}
116
116
117
117
type AppBskyEmbedImagesAggregated {
118
118
-
images: String
118
118
+
images: JSON
119
119
count: Int!
120
120
}
121
121
···
149
149
}
150
150
151
151
type AppBskyEmbedRecordAggregated {
152
152
-
record: String
152
152
+
record: JSON
153
153
count: Int!
154
154
}
155
155
···
184
184
}
185
185
186
186
type AppBskyEmbedRecordWithMediaAggregated {
187
187
-
media: String
188
188
-
record: String
187
187
+
media: JSON
188
188
+
record: JSON
189
189
count: Int!
190
190
}
191
191
···
222
222
}
223
223
224
224
type AppBskyEmbedVideoAggregated {
225
225
-
alt: String
226
226
-
aspectRatio: String
227
227
-
captions: String
228
228
-
video: String
225
225
+
alt: JSON
226
226
+
aspectRatio: JSON
227
227
+
captions: JSON
228
228
+
video: JSON
229
229
count: Int!
230
230
}
231
231
···
262
262
}
263
263
264
264
type AppBskyFeedPostgateAggregated {
265
265
-
createdAt: String
266
266
-
detachedEmbeddingUris: String
267
267
-
embeddingRules: String
268
268
-
post: String
265
265
+
createdAt: JSON
266
266
+
detachedEmbeddingUris: JSON
267
267
+
embeddingRules: JSON
268
268
+
post: JSON
269
269
count: Int!
270
270
}
271
271
···
302
302
}
303
303
304
304
type AppBskyFeedThreadgateAggregated {
305
305
-
allow: String
306
306
-
createdAt: String
307
307
-
hiddenReplies: String
308
308
-
post: String
305
305
+
allow: JSON
306
306
+
createdAt: JSON
307
307
+
hiddenReplies: JSON
308
308
+
post: JSON
309
309
count: Int!
310
310
}
311
311
···
340
340
}
341
341
342
342
type AppBskyRichtextFacetAggregated {
343
343
-
features: String
344
344
-
index: String
343
343
+
features: JSON
344
344
+
index: JSON
345
345
count: Int!
346
346
}
347
347
···
385
385
}
386
386
387
387
type ComAtprotoRepoStrongRefAggregated {
388
388
-
cid: String
389
389
-
uri: String
388
388
+
cid: JSON
389
389
+
uri: JSON
390
390
count: Int!
391
391
}
392
392
···
433
433
}
434
434
435
435
type FmTealAlphaFeedPlayAggregated {
436
436
-
artistMbIds: String
437
437
-
artistNames: String
438
438
-
artists: String
439
439
-
duration: String
440
440
-
isrc: String
441
441
-
musicServiceBaseDomain: String
442
442
-
originUrl: String
443
443
-
playedTime: String
444
444
-
recordingMbId: String
445
445
-
releaseMbId: String
446
446
-
releaseName: String
447
447
-
submissionClientAgent: String
448
448
-
trackMbId: String
449
449
-
trackName: String
436
436
+
artistMbIds: JSON
437
437
+
artistNames: JSON
438
438
+
artists: JSON
439
439
+
duration: JSON
440
440
+
isrc: JSON
441
441
+
musicServiceBaseDomain: JSON
442
442
+
originUrl: JSON
443
443
+
playedTime: JSON
444
444
+
recordingMbId: JSON
445
445
+
releaseMbId: JSON
446
446
+
releaseName: JSON
447
447
+
submissionClientAgent: JSON
448
448
+
trackMbId: JSON
449
449
+
trackName: JSON
450
450
count: Int!
451
451
}
452
452
···
574
574
input SortField {
575
575
field: String!
576
576
direction: SortDirection!
577
577
+
}
578
578
+
579
579
+
type Subscription {
580
580
+
"""Subscribe to app.bsky.feed.postgate record creation events"""
581
581
+
appBskyFeedPostgateCreated: AppBskyFeedPostgate!
582
582
+
583
583
+
"""Subscribe to app.bsky.feed.postgate record update events"""
584
584
+
appBskyFeedPostgateUpdated: AppBskyFeedPostgate!
585
585
+
586
586
+
"""
587
587
+
Subscribe to app.bsky.feed.postgate record deletion events. Returns the URI of deleted records.
588
588
+
"""
589
589
+
appBskyFeedPostgateDeleted: String!
590
590
+
591
591
+
"""Subscribe to app.bsky.feed.threadgate record creation events"""
592
592
+
appBskyFeedThreadgateCreated: AppBskyFeedThreadgate!
593
593
+
594
594
+
"""Subscribe to app.bsky.feed.threadgate record update events"""
595
595
+
appBskyFeedThreadgateUpdated: AppBskyFeedThreadgate!
596
596
+
597
597
+
"""
598
598
+
Subscribe to app.bsky.feed.threadgate record deletion events. Returns the URI of deleted records.
599
599
+
"""
600
600
+
appBskyFeedThreadgateDeleted: String!
601
601
+
602
602
+
"""Subscribe to app.bsky.actor.profile record creation events"""
603
603
+
appBskyActorProfileCreated: AppBskyActorProfile!
604
604
+
605
605
+
"""Subscribe to app.bsky.actor.profile record update events"""
606
606
+
appBskyActorProfileUpdated: AppBskyActorProfile!
607
607
+
608
608
+
"""
609
609
+
Subscribe to app.bsky.actor.profile record deletion events. Returns the URI of deleted records.
610
610
+
"""
611
611
+
appBskyActorProfileDeleted: String!
612
612
+
613
613
+
"""Subscribe to fm.teal.alpha.feed.play record creation events"""
614
614
+
fmTealAlphaFeedPlayCreated: FmTealAlphaFeedPlay!
615
615
+
616
616
+
"""Subscribe to fm.teal.alpha.feed.play record update events"""
617
617
+
fmTealAlphaFeedPlayUpdated: FmTealAlphaFeedPlay!
618
618
+
619
619
+
"""
620
620
+
Subscribe to fm.teal.alpha.feed.play record deletion events. Returns the URI of deleted records.
621
621
+
"""
622
622
+
fmTealAlphaFeedPlayDeleted: String!
577
623
}
578
624
579
625
type SyncResult {
+63
-2
src/App.tsx
···
1
1
-
import { graphql, useLazyLoadQuery, usePaginationFragment } from "react-relay";
1
1
+
import {
2
2
+
graphql,
3
3
+
useLazyLoadQuery,
4
4
+
usePaginationFragment,
5
5
+
useSubscription,
6
6
+
} from "react-relay";
2
7
import { useEffect, useRef } from "react";
3
8
import type { AppQuery } from "./__generated__/AppQuery.graphql";
4
9
import type { App_plays$key } from "./__generated__/App_plays.graphql";
10
10
+
import type { AppSubscription } from "./__generated__/AppSubscription.graphql";
5
11
import TrackItem from "./TrackItem";
6
12
import Layout from "./Layout";
13
13
+
import {
14
14
+
ConnectionHandler,
15
15
+
type GraphQLSubscriptionConfig,
16
16
+
} from "relay-runtime";
7
17
8
18
export default function App() {
9
19
const queryData = useLazyLoadQuery<AppQuery>(
···
46
56
47
57
const loadMoreRef = useRef<HTMLDivElement>(null);
48
58
59
59
+
// Subscribe to new plays
60
60
+
const subscriptionConfig: GraphQLSubscriptionConfig<AppSubscription> = {
61
61
+
subscription: graphql`
62
62
+
subscription AppSubscription {
63
63
+
fmTealAlphaFeedPlayCreated {
64
64
+
uri
65
65
+
playedTime
66
66
+
...TrackItem_play
67
67
+
}
68
68
+
}
69
69
+
`,
70
70
+
variables: {},
71
71
+
updater: (store) => {
72
72
+
const newPlay = store.getRootField("fmTealAlphaFeedPlayCreated");
73
73
+
if (!newPlay) return;
74
74
+
75
75
+
const root = store.getRoot();
76
76
+
const connection = ConnectionHandler.getConnection(
77
77
+
root,
78
78
+
"App_fmTealAlphaFeedPlays",
79
79
+
{ sortBy: [{ field: "playedTime", direction: "desc" }] }
80
80
+
);
81
81
+
82
82
+
if (!connection) return;
83
83
+
84
84
+
const edge = ConnectionHandler.createEdge(
85
85
+
store,
86
86
+
connection,
87
87
+
newPlay,
88
88
+
"FmTealAlphaFeedPlayEdge"
89
89
+
);
90
90
+
91
91
+
ConnectionHandler.insertEdgeBefore(connection, edge);
92
92
+
93
93
+
// Update totalCount
94
94
+
const totalCountRecord = root.getLinkedRecord("fmTealAlphaFeedPlays", {
95
95
+
sortBy: [{ field: "playedTime", direction: "desc" }],
96
96
+
});
97
97
+
if (totalCountRecord) {
98
98
+
const currentCount = totalCountRecord.getValue("totalCount") as number;
99
99
+
if (typeof currentCount === "number") {
100
100
+
totalCountRecord.setValue(currentCount + 1, "totalCount");
101
101
+
}
102
102
+
}
103
103
+
},
104
104
+
};
105
105
+
106
106
+
useSubscription(subscriptionConfig);
107
107
+
49
108
useEffect(() => {
50
109
window.scrollTo(0, 0);
51
110
}, []);
···
120
179
{hasNext && (
121
180
<div ref={loadMoreRef} className="py-12 text-center">
122
181
{isLoadingNext ? (
123
123
-
<p className="text-xs text-zinc-600 uppercase tracking-wider">Loading...</p>
182
182
+
<p className="text-xs text-zinc-600 uppercase tracking-wider">
183
183
+
Loading...
184
184
+
</p>
124
185
) : (
125
186
<p className="text-xs text-zinc-700 uppercase tracking-wider">·</p>
126
187
)}
+157
src/__generated__/AppSubscription.graphql.ts
···
1
1
+
/**
2
2
+
* @generated SignedSource<<401c6c4a1920db251447fa96aca8768a>>
3
3
+
* @lightSyntaxTransform
4
4
+
* @nogrep
5
5
+
*/
6
6
+
7
7
+
/* tslint:disable */
8
8
+
/* eslint-disable */
9
9
+
// @ts-nocheck
10
10
+
11
11
+
import { ConcreteRequest } from 'relay-runtime';
12
12
+
import { FragmentRefs } from "relay-runtime";
13
13
+
export type AppSubscription$variables = Record<PropertyKey, never>;
14
14
+
export type AppSubscription$data = {
15
15
+
readonly fmTealAlphaFeedPlayCreated: {
16
16
+
readonly playedTime: string | null | undefined;
17
17
+
readonly uri: string;
18
18
+
readonly " $fragmentSpreads": FragmentRefs<"TrackItem_play">;
19
19
+
};
20
20
+
};
21
21
+
export type AppSubscription = {
22
22
+
response: AppSubscription$data;
23
23
+
variables: AppSubscription$variables;
24
24
+
};
25
25
+
26
26
+
const node: ConcreteRequest = (function(){
27
27
+
var v0 = {
28
28
+
"alias": null,
29
29
+
"args": null,
30
30
+
"kind": "ScalarField",
31
31
+
"name": "uri",
32
32
+
"storageKey": null
33
33
+
},
34
34
+
v1 = {
35
35
+
"alias": null,
36
36
+
"args": null,
37
37
+
"kind": "ScalarField",
38
38
+
"name": "playedTime",
39
39
+
"storageKey": null
40
40
+
};
41
41
+
return {
42
42
+
"fragment": {
43
43
+
"argumentDefinitions": [],
44
44
+
"kind": "Fragment",
45
45
+
"metadata": null,
46
46
+
"name": "AppSubscription",
47
47
+
"selections": [
48
48
+
{
49
49
+
"alias": null,
50
50
+
"args": null,
51
51
+
"concreteType": "FmTealAlphaFeedPlay",
52
52
+
"kind": "LinkedField",
53
53
+
"name": "fmTealAlphaFeedPlayCreated",
54
54
+
"plural": false,
55
55
+
"selections": [
56
56
+
(v0/*: any*/),
57
57
+
(v1/*: any*/),
58
58
+
{
59
59
+
"args": null,
60
60
+
"kind": "FragmentSpread",
61
61
+
"name": "TrackItem_play"
62
62
+
}
63
63
+
],
64
64
+
"storageKey": null
65
65
+
}
66
66
+
],
67
67
+
"type": "Subscription",
68
68
+
"abstractKey": null
69
69
+
},
70
70
+
"kind": "Request",
71
71
+
"operation": {
72
72
+
"argumentDefinitions": [],
73
73
+
"kind": "Operation",
74
74
+
"name": "AppSubscription",
75
75
+
"selections": [
76
76
+
{
77
77
+
"alias": null,
78
78
+
"args": null,
79
79
+
"concreteType": "FmTealAlphaFeedPlay",
80
80
+
"kind": "LinkedField",
81
81
+
"name": "fmTealAlphaFeedPlayCreated",
82
82
+
"plural": false,
83
83
+
"selections": [
84
84
+
(v0/*: any*/),
85
85
+
(v1/*: any*/),
86
86
+
{
87
87
+
"alias": null,
88
88
+
"args": null,
89
89
+
"kind": "ScalarField",
90
90
+
"name": "trackName",
91
91
+
"storageKey": null
92
92
+
},
93
93
+
{
94
94
+
"alias": null,
95
95
+
"args": null,
96
96
+
"kind": "ScalarField",
97
97
+
"name": "artists",
98
98
+
"storageKey": null
99
99
+
},
100
100
+
{
101
101
+
"alias": null,
102
102
+
"args": null,
103
103
+
"kind": "ScalarField",
104
104
+
"name": "releaseName",
105
105
+
"storageKey": null
106
106
+
},
107
107
+
{
108
108
+
"alias": null,
109
109
+
"args": null,
110
110
+
"kind": "ScalarField",
111
111
+
"name": "releaseMbId",
112
112
+
"storageKey": null
113
113
+
},
114
114
+
{
115
115
+
"alias": null,
116
116
+
"args": null,
117
117
+
"kind": "ScalarField",
118
118
+
"name": "actorHandle",
119
119
+
"storageKey": null
120
120
+
},
121
121
+
{
122
122
+
"alias": null,
123
123
+
"args": null,
124
124
+
"concreteType": "AppBskyActorProfile",
125
125
+
"kind": "LinkedField",
126
126
+
"name": "appBskyActorProfile",
127
127
+
"plural": false,
128
128
+
"selections": [
129
129
+
{
130
130
+
"alias": null,
131
131
+
"args": null,
132
132
+
"kind": "ScalarField",
133
133
+
"name": "displayName",
134
134
+
"storageKey": null
135
135
+
}
136
136
+
],
137
137
+
"storageKey": null
138
138
+
}
139
139
+
],
140
140
+
"storageKey": null
141
141
+
}
142
142
+
]
143
143
+
},
144
144
+
"params": {
145
145
+
"cacheID": "c856872303e0f4904ea70ed5dc54cce2",
146
146
+
"id": null,
147
147
+
"metadata": {},
148
148
+
"name": "AppSubscription",
149
149
+
"operationKind": "subscription",
150
150
+
"text": "subscription AppSubscription {\n fmTealAlphaFeedPlayCreated {\n uri\n playedTime\n ...TrackItem_play\n }\n}\n\nfragment TrackItem_play on FmTealAlphaFeedPlay {\n trackName\n playedTime\n artists\n releaseName\n releaseMbId\n actorHandle\n appBskyActorProfile {\n displayName\n }\n}\n"
151
151
+
}
152
152
+
};
153
153
+
})();
154
154
+
155
155
+
(node as any).hash = "96fa73287dd5ff8b0c3e83b8f663f65e";
156
156
+
157
157
+
export default node;
+4
-4
src/__generated__/TopAlbumsQuery.graphql.ts
···
1
1
/**
2
2
-
* @generated SignedSource<<a492b6190b60e9be64d199702b76977a>>
2
2
+
* @generated SignedSource<<e2bf0d16ddd996a8b44b47387dd220b3>>
3
3
* @lightSyntaxTransform
4
4
* @nogrep
5
5
*/
···
12
12
export type TopAlbumsQuery$variables = Record<PropertyKey, never>;
13
13
export type TopAlbumsQuery$data = {
14
14
readonly fmTealAlphaFeedPlaysAggregated: ReadonlyArray<{
15
15
-
readonly artists: string | null | undefined;
15
15
+
readonly artists: any | null | undefined;
16
16
readonly count: number;
17
17
-
readonly releaseMbId: string | null | undefined;
18
18
-
readonly releaseName: string | null | undefined;
17
17
+
readonly releaseMbId: any | null | undefined;
18
18
+
readonly releaseName: any | null | undefined;
19
19
}>;
20
20
};
21
21
export type TopAlbumsQuery = {
+4
-4
src/__generated__/TopTracksQuery.graphql.ts
···
1
1
/**
2
2
-
* @generated SignedSource<<ed55197dadbf24b9cf975295d63a2436>>
2
2
+
* @generated SignedSource<<58e8aa653524405ace1405d28bd8f19e>>
3
3
* @lightSyntaxTransform
4
4
* @nogrep
5
5
*/
···
12
12
export type TopTracksQuery$variables = Record<PropertyKey, never>;
13
13
export type TopTracksQuery$data = {
14
14
readonly fmTealAlphaFeedPlaysAggregated: ReadonlyArray<{
15
15
-
readonly artists: string | null | undefined;
15
15
+
readonly artists: any | null | undefined;
16
16
readonly count: number;
17
17
-
readonly releaseMbId: string | null | undefined;
18
18
-
readonly trackName: string | null | undefined;
17
17
+
readonly releaseMbId: any | null | undefined;
18
18
+
readonly trackName: any | null | undefined;
19
19
}>;
20
20
};
21
21
export type TopTracksQuery = {
+67
-2
src/main.tsx
···
8
8
import TopAlbums from "./TopAlbums.tsx";
9
9
import LoadingFallback from "./LoadingFallback.tsx";
10
10
import { RelayEnvironmentProvider } from "react-relay";
11
11
-
import { Environment, Network, type FetchFunction } from "relay-runtime";
11
11
+
import {
12
12
+
Environment,
13
13
+
Network,
14
14
+
type FetchFunction,
15
15
+
Observable,
16
16
+
type SubscribeFunction,
17
17
+
type GraphQLResponse,
18
18
+
} from "relay-runtime";
19
19
+
import { createClient } from "graphql-ws";
12
20
13
21
const HTTP_ENDPOINT =
14
22
"https://api.slices.network/graphql?slice=at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a";
23
23
+
24
24
+
const WS_ENDPOINT =
25
25
+
"wss://api.slices.network/graphql/ws?slice=at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a";
15
26
16
27
const fetchGraphQL: FetchFunction = async (request, variables) => {
17
28
const resp = await fetch(HTTP_ENDPOINT, {
···
25
36
return await resp.json();
26
37
};
27
38
39
39
+
const wsClient = createClient({
40
40
+
url: WS_ENDPOINT,
41
41
+
retryAttempts: 5,
42
42
+
shouldRetry: () => true,
43
43
+
on: {
44
44
+
connected: () => {
45
45
+
console.log("WebSocket connected!");
46
46
+
},
47
47
+
error: (error) => {
48
48
+
console.error("WebSocket error:", error);
49
49
+
},
50
50
+
closed: (event) => {
51
51
+
console.log("WebSocket closed:", event);
52
52
+
},
53
53
+
},
54
54
+
});
55
55
+
56
56
+
const subscribe: SubscribeFunction = (operation, variables) => {
57
57
+
return Observable.create((sink) => {
58
58
+
if (!operation.text) {
59
59
+
sink.error(new Error("Missing operation text"));
60
60
+
return;
61
61
+
}
62
62
+
63
63
+
return wsClient.subscribe(
64
64
+
{
65
65
+
operationName: operation.name,
66
66
+
query: operation.text,
67
67
+
variables,
68
68
+
},
69
69
+
{
70
70
+
next: (data) => {
71
71
+
if (data.data !== null && data.data !== undefined) {
72
72
+
sink.next({ data: data.data } as GraphQLResponse);
73
73
+
}
74
74
+
},
75
75
+
error: (error) => {
76
76
+
console.error("Subscription error:", error);
77
77
+
if (error instanceof Error) {
78
78
+
sink.error(error);
79
79
+
} else if (error instanceof CloseEvent) {
80
80
+
sink.error(
81
81
+
new Error(`WebSocket closed: ${error.code} ${error.reason}`)
82
82
+
);
83
83
+
} else {
84
84
+
sink.error(new Error(JSON.stringify(error)));
85
85
+
}
86
86
+
},
87
87
+
complete: () => sink.complete(),
88
88
+
}
89
89
+
);
90
90
+
});
91
91
+
};
92
92
+
28
93
const environment = new Environment({
29
29
-
network: Network.create(fetchGraphQL),
94
94
+
network: Network.create(fetchGraphQL, subscribe),
30
95
});
31
96
32
97
createRoot(document.getElementById("root")!).render(