Teal.fm frontend powered by slices.network tealfm-slices.wisp.place
tealfm slices

add plays subscription

+8
deno.lock
···
"npm:eslint-plugin-react-refresh@~0.4.22": "0.4.23_eslint@9.36.0",
"npm:eslint@^9.36.0": "9.36.0",
"npm:globals@^16.4.0": "16.4.0",
+
"npm:graphql-ws@^6.0.6": "6.0.6_graphql@16.11.0",
"npm:graphql@^16.11.0": "16.11.0",
"npm:postcss@^8.5.6": "8.5.6",
"npm:react-dom@^19.1.1": "19.2.0_react@19.2.0",
···
"graphemer@1.4.0": {
"integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag=="
},
+
"graphql-ws@6.0.6_graphql@16.11.0": {
+
"integrity": "sha512-zgfER9s+ftkGKUZgc0xbx8T7/HMO4AV5/YuYiFc+AtgcO5T0v8AxYYNQ+ltzuzDZgNkYJaFspm5MMYLjQzrkmw==",
+
"dependencies": [
+
"graphql@16.11.0"
+
]
+
},
"graphql@15.3.0": {
"integrity": "sha512-GTCJtzJmkFLWRfFJuoo9RWWa/FfamUHgiFosxi/X1Ani4AVWbeyBenZTNX6dM+7WSbbFfTo/25eh0LLkwHMw2w=="
},
···
"npm:eslint-plugin-react-refresh@~0.4.22",
"npm:eslint@^9.36.0",
"npm:globals@^16.4.0",
+
"npm:graphql-ws@^6.0.6",
"npm:graphql@^16.11.0",
"npm:postcss@^8.5.6",
"npm:react-dom@^19.1.1",
+1
package.json
···
"schema:prod": "npx get-graphql-schema 'https://api.slices.network/graphql?slice=at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a' > schema.graphql"
},
"dependencies": {
+
"graphql-ws": "^6.0.6",
"react": "^19.1.1",
"react-dom": "^19.1.1",
"react-relay": "^20.1.1",
+89 -43
schema.graphql
···
}
type AppBskyActorProfileAggregated {
-
avatar: String
-
banner: String
-
createdAt: String
-
description: String
-
displayName: String
-
joinedViaStarterPack: String
-
labels: String
-
pinnedPost: String
+
avatar: JSON
+
banner: JSON
+
createdAt: JSON
+
description: JSON
+
displayName: JSON
+
joinedViaStarterPack: JSON
+
labels: JSON
+
pinnedPost: JSON
count: Int!
}
···
}
type AppBskyEmbedExternalAggregated {
-
external: String
+
external: JSON
count: Int!
}
···
}
type AppBskyEmbedImagesAggregated {
-
images: String
+
images: JSON
count: Int!
}
···
}
type AppBskyEmbedRecordAggregated {
-
record: String
+
record: JSON
count: Int!
}
···
}
type AppBskyEmbedRecordWithMediaAggregated {
-
media: String
-
record: String
+
media: JSON
+
record: JSON
count: Int!
}
···
}
type AppBskyEmbedVideoAggregated {
-
alt: String
-
aspectRatio: String
-
captions: String
-
video: String
+
alt: JSON
+
aspectRatio: JSON
+
captions: JSON
+
video: JSON
count: Int!
}
···
}
type AppBskyFeedPostgateAggregated {
-
createdAt: String
-
detachedEmbeddingUris: String
-
embeddingRules: String
-
post: String
+
createdAt: JSON
+
detachedEmbeddingUris: JSON
+
embeddingRules: JSON
+
post: JSON
count: Int!
}
···
}
type AppBskyFeedThreadgateAggregated {
-
allow: String
-
createdAt: String
-
hiddenReplies: String
-
post: String
+
allow: JSON
+
createdAt: JSON
+
hiddenReplies: JSON
+
post: JSON
count: Int!
}
···
}
type AppBskyRichtextFacetAggregated {
-
features: String
-
index: String
+
features: JSON
+
index: JSON
count: Int!
}
···
}
type ComAtprotoRepoStrongRefAggregated {
-
cid: String
-
uri: String
+
cid: JSON
+
uri: JSON
count: Int!
}
···
}
type FmTealAlphaFeedPlayAggregated {
-
artistMbIds: String
-
artistNames: String
-
artists: String
-
duration: String
-
isrc: String
-
musicServiceBaseDomain: String
-
originUrl: String
-
playedTime: String
-
recordingMbId: String
-
releaseMbId: String
-
releaseName: String
-
submissionClientAgent: String
-
trackMbId: String
-
trackName: String
+
artistMbIds: JSON
+
artistNames: JSON
+
artists: JSON
+
duration: JSON
+
isrc: JSON
+
musicServiceBaseDomain: JSON
+
originUrl: JSON
+
playedTime: JSON
+
recordingMbId: JSON
+
releaseMbId: JSON
+
releaseName: JSON
+
submissionClientAgent: JSON
+
trackMbId: JSON
+
trackName: JSON
count: Int!
}
···
input SortField {
field: String!
direction: SortDirection!
+
}
+
+
type Subscription {
+
"""Subscribe to app.bsky.feed.postgate record creation events"""
+
appBskyFeedPostgateCreated: AppBskyFeedPostgate!
+
+
"""Subscribe to app.bsky.feed.postgate record update events"""
+
appBskyFeedPostgateUpdated: AppBskyFeedPostgate!
+
+
"""
+
Subscribe to app.bsky.feed.postgate record deletion events. Returns the URI of deleted records.
+
"""
+
appBskyFeedPostgateDeleted: String!
+
+
"""Subscribe to app.bsky.feed.threadgate record creation events"""
+
appBskyFeedThreadgateCreated: AppBskyFeedThreadgate!
+
+
"""Subscribe to app.bsky.feed.threadgate record update events"""
+
appBskyFeedThreadgateUpdated: AppBskyFeedThreadgate!
+
+
"""
+
Subscribe to app.bsky.feed.threadgate record deletion events. Returns the URI of deleted records.
+
"""
+
appBskyFeedThreadgateDeleted: String!
+
+
"""Subscribe to app.bsky.actor.profile record creation events"""
+
appBskyActorProfileCreated: AppBskyActorProfile!
+
+
"""Subscribe to app.bsky.actor.profile record update events"""
+
appBskyActorProfileUpdated: AppBskyActorProfile!
+
+
"""
+
Subscribe to app.bsky.actor.profile record deletion events. Returns the URI of deleted records.
+
"""
+
appBskyActorProfileDeleted: String!
+
+
"""Subscribe to fm.teal.alpha.feed.play record creation events"""
+
fmTealAlphaFeedPlayCreated: FmTealAlphaFeedPlay!
+
+
"""Subscribe to fm.teal.alpha.feed.play record update events"""
+
fmTealAlphaFeedPlayUpdated: FmTealAlphaFeedPlay!
+
+
"""
+
Subscribe to fm.teal.alpha.feed.play record deletion events. Returns the URI of deleted records.
+
"""
+
fmTealAlphaFeedPlayDeleted: String!
}
type SyncResult {
+63 -2
src/App.tsx
···
-
import { graphql, useLazyLoadQuery, usePaginationFragment } from "react-relay";
+
import {
+
graphql,
+
useLazyLoadQuery,
+
usePaginationFragment,
+
useSubscription,
+
} from "react-relay";
import { useEffect, useRef } from "react";
import type { AppQuery } from "./__generated__/AppQuery.graphql";
import type { App_plays$key } from "./__generated__/App_plays.graphql";
+
import type { AppSubscription } from "./__generated__/AppSubscription.graphql";
import TrackItem from "./TrackItem";
import Layout from "./Layout";
+
import {
+
ConnectionHandler,
+
type GraphQLSubscriptionConfig,
+
} from "relay-runtime";
export default function App() {
const queryData = useLazyLoadQuery<AppQuery>(
···
const loadMoreRef = useRef<HTMLDivElement>(null);
+
// Subscribe to new plays
+
const subscriptionConfig: GraphQLSubscriptionConfig<AppSubscription> = {
+
subscription: graphql`
+
subscription AppSubscription {
+
fmTealAlphaFeedPlayCreated {
+
uri
+
playedTime
+
...TrackItem_play
+
}
+
}
+
`,
+
variables: {},
+
updater: (store) => {
+
const newPlay = store.getRootField("fmTealAlphaFeedPlayCreated");
+
if (!newPlay) return;
+
+
const root = store.getRoot();
+
const connection = ConnectionHandler.getConnection(
+
root,
+
"App_fmTealAlphaFeedPlays",
+
{ sortBy: [{ field: "playedTime", direction: "desc" }] }
+
);
+
+
if (!connection) return;
+
+
const edge = ConnectionHandler.createEdge(
+
store,
+
connection,
+
newPlay,
+
"FmTealAlphaFeedPlayEdge"
+
);
+
+
ConnectionHandler.insertEdgeBefore(connection, edge);
+
+
// Update totalCount
+
const totalCountRecord = root.getLinkedRecord("fmTealAlphaFeedPlays", {
+
sortBy: [{ field: "playedTime", direction: "desc" }],
+
});
+
if (totalCountRecord) {
+
const currentCount = totalCountRecord.getValue("totalCount") as number;
+
if (typeof currentCount === "number") {
+
totalCountRecord.setValue(currentCount + 1, "totalCount");
+
}
+
}
+
},
+
};
+
+
useSubscription(subscriptionConfig);
+
useEffect(() => {
window.scrollTo(0, 0);
}, []);
···
{hasNext && (
<div ref={loadMoreRef} className="py-12 text-center">
{isLoadingNext ? (
-
<p className="text-xs text-zinc-600 uppercase tracking-wider">Loading...</p>
+
<p className="text-xs text-zinc-600 uppercase tracking-wider">
+
Loading...
+
</p>
) : (
<p className="text-xs text-zinc-700 uppercase tracking-wider">·</p>
)}
+157
src/__generated__/AppSubscription.graphql.ts
···
+
/**
+
* @generated SignedSource<<401c6c4a1920db251447fa96aca8768a>>
+
* @lightSyntaxTransform
+
* @nogrep
+
*/
+
+
/* tslint:disable */
+
/* eslint-disable */
+
// @ts-nocheck
+
+
import { ConcreteRequest } from 'relay-runtime';
+
import { FragmentRefs } from "relay-runtime";
+
export type AppSubscription$variables = Record<PropertyKey, never>;
+
export type AppSubscription$data = {
+
readonly fmTealAlphaFeedPlayCreated: {
+
readonly playedTime: string | null | undefined;
+
readonly uri: string;
+
readonly " $fragmentSpreads": FragmentRefs<"TrackItem_play">;
+
};
+
};
+
export type AppSubscription = {
+
response: AppSubscription$data;
+
variables: AppSubscription$variables;
+
};
+
+
const node: ConcreteRequest = (function(){
+
var v0 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "uri",
+
"storageKey": null
+
},
+
v1 = {
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "playedTime",
+
"storageKey": null
+
};
+
return {
+
"fragment": {
+
"argumentDefinitions": [],
+
"kind": "Fragment",
+
"metadata": null,
+
"name": "AppSubscription",
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"concreteType": "FmTealAlphaFeedPlay",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlayCreated",
+
"plural": false,
+
"selections": [
+
(v0/*: any*/),
+
(v1/*: any*/),
+
{
+
"args": null,
+
"kind": "FragmentSpread",
+
"name": "TrackItem_play"
+
}
+
],
+
"storageKey": null
+
}
+
],
+
"type": "Subscription",
+
"abstractKey": null
+
},
+
"kind": "Request",
+
"operation": {
+
"argumentDefinitions": [],
+
"kind": "Operation",
+
"name": "AppSubscription",
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"concreteType": "FmTealAlphaFeedPlay",
+
"kind": "LinkedField",
+
"name": "fmTealAlphaFeedPlayCreated",
+
"plural": false,
+
"selections": [
+
(v0/*: any*/),
+
(v1/*: any*/),
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "trackName",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "artists",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "releaseName",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "releaseMbId",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "actorHandle",
+
"storageKey": null
+
},
+
{
+
"alias": null,
+
"args": null,
+
"concreteType": "AppBskyActorProfile",
+
"kind": "LinkedField",
+
"name": "appBskyActorProfile",
+
"plural": false,
+
"selections": [
+
{
+
"alias": null,
+
"args": null,
+
"kind": "ScalarField",
+
"name": "displayName",
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
}
+
],
+
"storageKey": null
+
}
+
]
+
},
+
"params": {
+
"cacheID": "c856872303e0f4904ea70ed5dc54cce2",
+
"id": null,
+
"metadata": {},
+
"name": "AppSubscription",
+
"operationKind": "subscription",
+
"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"
+
}
+
};
+
})();
+
+
(node as any).hash = "96fa73287dd5ff8b0c3e83b8f663f65e";
+
+
export default node;
+4 -4
src/__generated__/TopAlbumsQuery.graphql.ts
···
/**
-
* @generated SignedSource<<a492b6190b60e9be64d199702b76977a>>
+
* @generated SignedSource<<e2bf0d16ddd996a8b44b47387dd220b3>>
* @lightSyntaxTransform
* @nogrep
*/
···
export type TopAlbumsQuery$variables = Record<PropertyKey, never>;
export type TopAlbumsQuery$data = {
readonly fmTealAlphaFeedPlaysAggregated: ReadonlyArray<{
-
readonly artists: string | null | undefined;
+
readonly artists: any | null | undefined;
readonly count: number;
-
readonly releaseMbId: string | null | undefined;
-
readonly releaseName: string | null | undefined;
+
readonly releaseMbId: any | null | undefined;
+
readonly releaseName: any | null | undefined;
}>;
};
export type TopAlbumsQuery = {
+4 -4
src/__generated__/TopTracksQuery.graphql.ts
···
/**
-
* @generated SignedSource<<ed55197dadbf24b9cf975295d63a2436>>
+
* @generated SignedSource<<58e8aa653524405ace1405d28bd8f19e>>
* @lightSyntaxTransform
* @nogrep
*/
···
export type TopTracksQuery$variables = Record<PropertyKey, never>;
export type TopTracksQuery$data = {
readonly fmTealAlphaFeedPlaysAggregated: ReadonlyArray<{
-
readonly artists: string | null | undefined;
+
readonly artists: any | null | undefined;
readonly count: number;
-
readonly releaseMbId: string | null | undefined;
-
readonly trackName: string | null | undefined;
+
readonly releaseMbId: any | null | undefined;
+
readonly trackName: any | null | undefined;
}>;
};
export type TopTracksQuery = {
+67 -2
src/main.tsx
···
import TopAlbums from "./TopAlbums.tsx";
import LoadingFallback from "./LoadingFallback.tsx";
import { RelayEnvironmentProvider } from "react-relay";
-
import { Environment, Network, type FetchFunction } from "relay-runtime";
+
import {
+
Environment,
+
Network,
+
type FetchFunction,
+
Observable,
+
type SubscribeFunction,
+
type GraphQLResponse,
+
} from "relay-runtime";
+
import { createClient } from "graphql-ws";
const HTTP_ENDPOINT =
"https://api.slices.network/graphql?slice=at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a";
+
+
const WS_ENDPOINT =
+
"wss://api.slices.network/graphql/ws?slice=at://did:plc:fpruhuo22xkm5o7ttr2ktxdo/network.slices.slice/3m257yljpbg2a";
const fetchGraphQL: FetchFunction = async (request, variables) => {
const resp = await fetch(HTTP_ENDPOINT, {
···
return await resp.json();
};
+
const wsClient = createClient({
+
url: WS_ENDPOINT,
+
retryAttempts: 5,
+
shouldRetry: () => true,
+
on: {
+
connected: () => {
+
console.log("WebSocket connected!");
+
},
+
error: (error) => {
+
console.error("WebSocket error:", error);
+
},
+
closed: (event) => {
+
console.log("WebSocket closed:", event);
+
},
+
},
+
});
+
+
const subscribe: SubscribeFunction = (operation, variables) => {
+
return Observable.create((sink) => {
+
if (!operation.text) {
+
sink.error(new Error("Missing operation text"));
+
return;
+
}
+
+
return wsClient.subscribe(
+
{
+
operationName: operation.name,
+
query: operation.text,
+
variables,
+
},
+
{
+
next: (data) => {
+
if (data.data !== null && data.data !== undefined) {
+
sink.next({ data: data.data } as GraphQLResponse);
+
}
+
},
+
error: (error) => {
+
console.error("Subscription error:", error);
+
if (error instanceof Error) {
+
sink.error(error);
+
} else if (error instanceof CloseEvent) {
+
sink.error(
+
new Error(`WebSocket closed: ${error.code} ${error.reason}`)
+
);
+
} else {
+
sink.error(new Error(JSON.stringify(error)));
+
}
+
},
+
complete: () => sink.complete(),
+
}
+
);
+
});
+
};
+
const environment = new Environment({
-
network: Network.create(fetchGraphQL),
+
network: Network.create(fetchGraphQL, subscribe),
});
createRoot(document.getElementById("root")!).render(