Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feature: Support defer and stream GraphQL Directives in RedwoodRealtime #9235

Merged
merged 25 commits into from
Oct 31, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
af0d080
Adds useDeferStream to Realtime
dthyresson Sep 25, 2023
801990c
Don't overwrite the result with just data and errors to support repea…
dthyresson Sep 25, 2023
ee4a4ea
change option to enableDeferStream
dthyresson Sep 25, 2023
f1a6c8a
Adds defer and stream examples to RT setup
dthyresson Sep 25, 2023
ba27ce4
Rework defer and stream example templates
dthyresson Sep 25, 2023
1423fc8
countdown example now uses Repeater
dthyresson Sep 25, 2023
fd7966a
Merge branch 'main' into dt-realtime-defer-stream
dthyresson Sep 25, 2023
8fc3e19
Merge branch 'main' into dt-realtime-defer-stream
dthyresson Oct 16, 2023
fdf42b0
Documents realtime and defer and stream
dthyresson Oct 16, 2023
471cb34
clarify docs of SSE and serverful deploy
dthyresson Oct 16, 2023
8bb557b
Merge branch 'main' into dt-realtime-defer-stream
dthyresson Oct 20, 2023
5332b66
Updates realtime config docs
dthyresson Oct 20, 2023
561b2ab
Support schema coordinates. Adds enable defer to template
dthyresson Oct 23, 2023
ef65e05
Merge branch 'main' into dt-realtime-defer-stream
dthyresson Oct 23, 2023
53b2b7c
Update docs/docs/realtime.md
dthyresson Oct 31, 2023
5244e11
Update docs/docs/realtime.md
dthyresson Oct 31, 2023
bdf80bd
Remove duplicate enableDeferStream
dthyresson Oct 31, 2023
7576e9d
Update docs/docs/realtime.md
dthyresson Oct 31, 2023
7f1081b
Update docs/docs/realtime.md
dthyresson Oct 31, 2023
d8e0432
Update docs/docs/realtime.md
dthyresson Oct 31, 2023
83e4211
Update docs/docs/realtime.md
dthyresson Oct 31, 2023
b7748db
Delete packages/cli/src/commands/experimental/templates/defer/.keep
dthyresson Oct 31, 2023
c476f75
Delete packages/cli/src/commands/experimental/templates/stream/.keep
dthyresson Oct 31, 2023
41d4b16
Merge branch 'main' into dt-realtime-defer-stream
dthyresson Oct 31, 2023
8489f2d
Adds examples for stream and defer in docs
dthyresson Oct 31, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
709 changes: 709 additions & 0 deletions docs/docs/realtime.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/sidebars.js
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,7 @@ module.exports = {
'prerender',
'project-configuration-dev-test-build',
'redwoodrecord',
'realtime',
'router',
'schema-relations',
'security',
Expand Down
120 changes: 120 additions & 0 deletions packages/cli/src/commands/experimental/setupRealtimeHandler.js
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,126 @@ export async function handler({ force, includeExamples, verbose }) {
]
},
},

{
title: 'Adding Defer example queries ...',
enabled: () => includeExamples,
task: () => {
// sdl

const exampleSdlTemplateContent = fs.readFileSync(
path.resolve(
__dirname,
'templates',
'defer',
'fastAndSlowFields',
`fastAndSlowFields.sdl.template`
),
'utf-8'
)

const sdlFile = path.join(
redwoodPaths.api.graphql,
`fastAndSlowFields.sdl.${isTypeScriptProject() ? 'ts' : 'js'}`
)

const sdlContent = ts
? exampleSdlTemplateContent
: transformTSToJS(sdlFile, exampleSdlTemplateContent)

// service

const exampleServiceTemplateContent = fs.readFileSync(
path.resolve(
__dirname,
'templates',
'defer',
'fastAndSlowFields',
`fastAndSlowFields.ts.template`
),
'utf-8'
)
const serviceFile = path.join(
redwoodPaths.api.services,
'fastAndSlowFields',
`fastAndSlowFields.${isTypeScriptProject() ? 'ts' : 'js'}`
)

const serviceContent = ts
? exampleServiceTemplateContent
: transformTSToJS(serviceFile, exampleServiceTemplateContent)

// write all files
return [
writeFile(sdlFile, sdlContent, {
overwriteExisting: force,
}),
writeFile(serviceFile, serviceContent, {
overwriteExisting: force,
}),
]
},
},

{
title: 'Adding Stream example queries ...',
enabled: () => includeExamples,
task: () => {
// sdl

const exampleSdlTemplateContent = fs.readFileSync(
path.resolve(
__dirname,
'templates',
'stream',
'alphabet',
`alphabet.sdl.template`
),
'utf-8'
)

const sdlFile = path.join(
redwoodPaths.api.graphql,
`alphabet.sdl.${isTypeScriptProject() ? 'ts' : 'js'}`
)

const sdlContent = ts
? exampleSdlTemplateContent
: transformTSToJS(sdlFile, exampleSdlTemplateContent)

// service

const exampleServiceTemplateContent = fs.readFileSync(
path.resolve(
__dirname,
'templates',
'stream',
'alphabet',
`alphabet.ts.template`
),
'utf-8'
)
const serviceFile = path.join(
redwoodPaths.api.services,
'alphabet',
`alphabet.${isTypeScriptProject() ? 'ts' : 'js'}`
)

const serviceContent = ts
? exampleServiceTemplateContent
: transformTSToJS(serviceFile, exampleServiceTemplateContent)

// write all files
return [
writeFile(sdlFile, sdlContent, {
overwriteExisting: force,
}),
writeFile(serviceFile, serviceContent, {
overwriteExisting: force,
}),
]
},
},
{
title: 'Adding config to redwood.toml...',
task: (_ctx, task) => {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export const schema = gql`
type Query {
"""
A field that resolves fast.
"""
fastField: String! @skipAuth

"""
A field that resolves slowly.
Maybe you want to @defer this field ;)
"""
slowField(waitFor: Int! = 5000): String @skipAuth
}
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { logger } from 'src/lib/logger'

const wait = (time: number) =>
new Promise((resolve) => setTimeout(resolve, time))

export const fastField = async () => {
return 'I am fast'
}

export const slowField = async (_, { waitFor = 5000 }) => {
logger.debug('waiting on slowField')
await wait(waitFor)
return 'I am slow'
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ import subscriptions from 'src/subscriptions/**/*.{js,ts}'
* Redwood Realtime
* - uses a publish/subscribe model to broadcast data to clients.
* - uses a store to persist Live Query and Subscription data.
* - and enable defer and stream directives to improve latency
* for clients by sending data the most important data as soon as it's ready.
*
* Redwood Realtime supports in-memory and Redis stores:
* - In-memory stores are useful for development and testing.
Expand All @@ -39,4 +41,6 @@ export const realtime: RedwoodRealtimeOptions = {
// if using a Redis store
// store: { redis: { publishClient, subscribeClient } },
},
// To enable defer and streaming, set to true.
// enableDeferStream: true,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export const schema = gql`
type Query {
"""
A field that spells out the letters of the alphabet
Maybe you want to @stream this field ;)
"""
alphabet: [String!]! @skipAuth
}
`
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import { Repeater } from '@redwoodjs/realtime'

import { logger } from 'src/lib/logger'

export const alphabet = async () => {
return new Repeater<string>(async (push, stop) => {
const letters = 'abcdefghijklmnopqrstuvwxyz'.split('')

const publish = () => {
const letter = letters.shift()

if (letter) {
logger.debug({ letter }, 'publishing letter...')
push(letter)
}

if (letters.length === 0) {
stop()
}
}

const interval = setInterval(publish, 1000)

stop.then(() => {
logger.debug('cancel')
clearInterval(interval)
})

publish()
})
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
import gql from 'graphql-tag'

import { Repeater } from '@redwoodjs/realtime'

import { logger } from 'src/lib/logger'

export const schema = gql`
type Subscription {
countdown(from: Int!, interval: Int!): Int! @requireAuth
Expand All @@ -15,14 +19,39 @@ export const schema = gql`
*/
const countdown = {
countdown: {
async *subscribe(_, { from = 100, interval = 10 }) {
while (from >= 0) {
yield { countdown: from }
// pause for 1/4 second
await new Promise((resolve) => setTimeout(resolve, 250))
from -= interval
subscribe: (
_,
{
from = 100,
interval = 10,
}: {
from: number
interval: number
}
},
) =>
new Repeater((push, stop) => {
function decrement() {
from -= interval

if (from < 0) {
logger.debug({ from }, 'stopping as countdown is less than 0')
stop()
}

logger.debug({ from }, 'pushing countdown value ...')
push(from)
}

decrement()

const delay = setInterval(decrement, 500)

stop.then(() => {
clearInterval(delay)
logger.debug('stopping countdown')
})
}),
resolve: (payload: number) => payload,
},
}

Expand Down
6 changes: 5 additions & 1 deletion packages/graphql-server/src/plugins/useRedwoodError.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,12 @@ export const useRedwoodError = (
}
})

// be certain to return the complete result
// and not just the data or the errors
// because defer, stream and AsyncIterator results
// need to be returned as is
setResult({
data: result.data,
...result,
errors,
extensions: result.extensions || {},
})
Expand Down
1 change: 1 addition & 0 deletions packages/realtime/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@
"@envelop/live-query": "6.0.0",
"@graphql-tools/schema": "10.0.0",
"@graphql-tools/utils": "10.0.1",
"@graphql-yoga/plugin-defer-stream": "2.0.4",
"@graphql-yoga/plugin-graphql-sse": "2.0.4",
"@graphql-yoga/redis-event-target": "2.0.0",
"@graphql-yoga/subscription": "4.0.0",
Expand Down
1 change: 1 addition & 0 deletions packages/realtime/src/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export {
RedisLiveQueryStore,
liveQueryStore,
pubSub,
Repeater,
} from './plugins/useRedwoodRealtime'

export type {
Expand Down
7 changes: 7 additions & 0 deletions packages/realtime/src/graphql/plugins/useRedwoodRealtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Plugin } from '@envelop/core'
import { useLiveQuery } from '@envelop/live-query'
import { mergeSchemas } from '@graphql-tools/schema'
import { astFromDirective } from '@graphql-tools/utils'
import { useDeferStream } from '@graphql-yoga/plugin-defer-stream'
import { useGraphQLSSE } from '@graphql-yoga/plugin-graphql-sse'
import { createRedisEventTarget } from '@graphql-yoga/redis-event-target'
import type { CreateRedisEventTargetArgs } from '@graphql-yoga/redis-event-target'
Expand All @@ -12,6 +13,8 @@ import { InMemoryLiveQueryStore } from '@n1ru4l/in-memory-live-query-store'
import type { execute as defaultExecute } from 'graphql'
import { print } from 'graphql'

export { Repeater } from 'graphql-yoga'

/**
* We want SubscriptionsGlobs type to be an object with this shape:
*
Expand Down Expand Up @@ -60,6 +63,7 @@ export type SubscribeClientType = CreateRedisEventTargetArgs['subscribeClient']
*
*/
export type RedwoodRealtimeOptions = {
enableDeferStream?: boolean
liveQueries?: {
/**
* @description Redwood Realtime supports in-memory and Redis stores.
Expand Down Expand Up @@ -232,6 +236,9 @@ export const useRedwoodRealtime = (options: RedwoodRealtimeOptions): Plugin => {
if (subscriptionsEnabled) {
addPlugin(useGraphQLSSE() as Plugin<object>)
}
if (options.enableDeferStream) {
addPlugin(useDeferStream() as Plugin<object>)
}
},
onContextBuilding() {
return ({ extendContext }) => {
Expand Down
1 change: 1 addition & 0 deletions packages/realtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export {
RedisLiveQueryStore,
liveQueryStore,
pubSub,
Repeater,
} from './graphql'

export type {
Expand Down
13 changes: 13 additions & 0 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -4455,6 +4455,18 @@ __metadata:
languageName: node
linkType: hard

"@graphql-yoga/plugin-defer-stream@npm:2.0.4":
version: 2.0.4
resolution: "@graphql-yoga/plugin-defer-stream@npm:2.0.4"
dependencies:
"@graphql-tools/utils": ^10.0.0
peerDependencies:
graphql: ^15.2.0 || ^16.0.0
graphql-yoga: ^4.0.4
checksum: d402809bb5ef9bdb1aea3376bc18d756246852326a7d630930d0ea1630ebdca2e82d61bfa5123efaa69514ae37f3e02c20043a2512b82000262ffb3e33b17596
languageName: node
linkType: hard

"@graphql-yoga/plugin-graphql-sse@npm:2.0.4":
version: 2.0.4
resolution: "@graphql-yoga/plugin-graphql-sse@npm:2.0.4"
Expand Down Expand Up @@ -8947,6 +8959,7 @@ __metadata:
"@envelop/types": 4.0.0
"@graphql-tools/schema": 10.0.0
"@graphql-tools/utils": 10.0.1
"@graphql-yoga/plugin-defer-stream": 2.0.4
"@graphql-yoga/plugin-graphql-sse": 2.0.4
"@graphql-yoga/redis-event-target": 2.0.0
"@graphql-yoga/subscription": 4.0.0
Expand Down
Loading