-
Notifications
You must be signed in to change notification settings - Fork 304
SSR #1905
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
base: main
Are you sure you want to change the base?
SSR #1905
Changes from all commits
9c65903
4bed475
d5e7b0c
5a99cb2
2d4217d
6c3b201
f0754af
a3865cc
bff7311
8de1cdb
ef2de43
c3fd862
5a9ee8f
397bdaf
a696548
cf3d533
532a5d7
84f0dc7
892dd86
40c28ed
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,123 @@ | ||
| import { expect, test } from 'vitest'; | ||
| import { i } from '../../src/schema'; | ||
| import { parseSchemaFromJSON } from '../../src/parseSchemaFromJSON'; | ||
| import { InstantSchemaDef } from '../../src/schemaTypes'; | ||
|
|
||
| const schema = i.schema({ | ||
| entities: { | ||
| users: i.entity({ | ||
| name: i.string(), | ||
| email: i.string().indexed().unique(), | ||
| bio: i.string().optional(), | ||
| // this is a convenient way to typecheck custom JSON fields | ||
| // though we should probably have a backend solution for this | ||
| stuff: i.json<{ custom: string }>(), | ||
| junk: i.any(), | ||
| }), | ||
| posts: i.entity({ | ||
| title: i.string().optional(), | ||
| body: i.string(), | ||
| }), | ||
| comments: i.entity({ | ||
| body: i.string().indexed(), | ||
| likes: i.number(), | ||
| }), | ||
|
|
||
| birthdays: i.entity({ | ||
| date: i.date(), | ||
| message: i.string(), | ||
| prizes: i.json<string | number>(), | ||
| }), | ||
| }, | ||
| links: { | ||
| usersPosts: { | ||
| forward: { | ||
| on: 'users', | ||
| has: 'many', | ||
| label: 'posts', | ||
| }, | ||
| reverse: { | ||
| on: 'posts', | ||
| has: 'one', | ||
| label: 'author', | ||
| }, | ||
| }, | ||
| postsComments: { | ||
| forward: { | ||
| on: 'posts', | ||
| has: 'many', | ||
| label: 'comments', | ||
| }, | ||
| reverse: { | ||
| on: 'comments', | ||
| has: 'one', | ||
| label: 'post', | ||
| }, | ||
| }, | ||
| friendships: { | ||
| forward: { | ||
| on: 'users', | ||
| has: 'many', | ||
| label: 'friends', | ||
| }, | ||
| reverse: { | ||
| on: 'users', | ||
| has: 'many', | ||
| label: '_friends', | ||
| }, | ||
| }, | ||
| referrals: { | ||
| forward: { | ||
| on: 'users', | ||
| has: 'many', | ||
| label: 'referred', | ||
| }, | ||
| reverse: { | ||
| on: 'users', | ||
| has: 'one', | ||
| label: 'referrer', | ||
| }, | ||
| }, | ||
| }, | ||
| rooms: { | ||
| chat: { | ||
| presence: i.entity({ | ||
| name: i.string(), | ||
| status: i.string(), | ||
| }), | ||
| topics: { | ||
| sendEmoji: i.entity({ | ||
| emoji: i.string(), | ||
| }), | ||
| }, | ||
| }, | ||
| }, | ||
| }); | ||
|
|
||
| type AnySchema = InstantSchemaDef<any, any, any>; | ||
|
|
||
| // compare schemas by stringifying them with json and comparing the strings | ||
| const compareSchemas = (schema1: AnySchema, schema2: AnySchema) => { | ||
| expect(JSON.stringify(schema1, null, 2)).toBe( | ||
| JSON.stringify(schema2, null, 2), | ||
| ); | ||
| }; | ||
|
|
||
| test('stuff', () => { | ||
| const stringified = JSON.stringify(schema, null, 2); | ||
| const parsed = JSON.parse(stringified); | ||
| console.log(stringified); | ||
|
|
||
| const otherSide = parseSchemaFromJSON(parsed); | ||
|
|
||
| compareSchemas(schema, otherSide); | ||
|
|
||
| expect(schema.entities.comments.links).toEqual( | ||
| otherSide.entities.comments.links, | ||
| ); | ||
| expect(schema.entities.comments.asType).toEqual( | ||
| otherSide.entities.comments.asType, | ||
| ); | ||
|
|
||
| expect(schema).toStrictEqual(otherSide); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -275,7 +275,9 @@ export default class Reactor { | |
| this._oauthCallbackResponse = this._oauthLoginInit(); | ||
|
|
||
| // kick off a request to cache it | ||
| this.getCurrentUser(); | ||
| this.getCurrentUser().then((userInfo) => { | ||
| this.syncUserToEndpoint(userInfo.user); | ||
| }); | ||
|
|
||
| NetworkListener.getIsOnline().then((isOnline) => { | ||
| this._isOnline = isOnline; | ||
|
|
@@ -490,6 +492,43 @@ export default class Reactor { | |
| } | ||
| } | ||
|
|
||
| /** | ||
| * Does the same thing as handle-query-add-ok | ||
| * but called as a result of receiving query info from ssr | ||
| * @param {any} q | ||
| * @param {{ triples: any; pageInfo: any; }} result | ||
| * @param {boolean} enableCardinalityInference | ||
| */ | ||
| _addQueryData(q, result, enableCardinalityInference) { | ||
| if (!this.attrs) { | ||
| throw new Error('Attrs in reactor have not been set'); | ||
| } | ||
| const queryHash = weakHash(q); | ||
| const store = s.createStore( | ||
| this.attrs, | ||
| result.triples, | ||
| enableCardinalityInference, | ||
| this._linkIndex, | ||
| this.config.useDateObjects, | ||
| ); | ||
| this.querySubs.set((prev) => { | ||
| prev[queryHash] = { | ||
| result: { | ||
| store, | ||
| pageInfo: result.pageInfo, | ||
| processedTxId: undefined, | ||
| isExternal: true, | ||
| }, | ||
| q, | ||
| }; | ||
| return prev; | ||
| }); | ||
| this._cleanupPendingMutationsQueries(); | ||
| this.notifyOne(queryHash); | ||
| this.notifyOneQueryOnce(queryHash); | ||
| this._cleanupPendingMutationsTimeout(); | ||
| } | ||
|
|
||
| _handleReceive(connId, msg) { | ||
| // opt-out, enabled by default if schema | ||
| const enableCardinalityInference = | ||
|
|
@@ -1065,7 +1104,7 @@ export default class Reactor { | |
| } | ||
|
|
||
| /** Runs instaql on a query and a store */ | ||
| dataForQuery(hash) { | ||
| dataForQuery(hash, applyOptimistic = true) { | ||
| const errorMessage = this._errorMessage; | ||
| if (errorMessage) { | ||
| return { error: errorMessage }; | ||
|
|
@@ -1089,17 +1128,16 @@ export default class Reactor { | |
| return cached.data; | ||
| } | ||
|
|
||
| const { store, pageInfo, aggregate, processedTxId } = result; | ||
| let store = result.store; | ||
| const { pageInfo, aggregate, processedTxId } = result; | ||
| const mutations = this._rewriteMutationsSorted( | ||
| store.attrs, | ||
| pendingMutations, | ||
| ); | ||
| const newStore = this._applyOptimisticUpdates( | ||
| store, | ||
| mutations, | ||
| processedTxId, | ||
| ); | ||
| const resp = instaql({ store: newStore, pageInfo, aggregate }, q); | ||
| if (applyOptimistic) { | ||
| store = this._applyOptimisticUpdates(store, mutations, processedTxId); | ||
| } | ||
| const resp = instaql({ store: store, pageInfo, aggregate }, q); | ||
|
|
||
| this._dataForQueryCache[hash] = { | ||
| querySubVersion, | ||
|
|
@@ -1817,7 +1855,27 @@ export default class Reactor { | |
| } | ||
| } | ||
|
|
||
| async syncUserToEndpoint(user) { | ||
| if (this.config.endpointURI) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. endpointURI seems too generic There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. i partially intentionally left it generic to account for more possible webhook actions in the future, also can't really think of a good name |
||
| try { | ||
| fetch(this.config.endpointURI + '/sync-auth', { | ||
| method: 'POST', | ||
| body: JSON.stringify({ | ||
| user: user, | ||
| }), | ||
| headers: { | ||
| 'Content-Type': 'application/json', | ||
| }, | ||
| }); | ||
| } catch (error) { | ||
| console.error('Error syncing user with external endpoint', error); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| updateUser(newUser) { | ||
| this.syncUserToEndpoint(newUser); | ||
|
|
||
| const newV = { error: undefined, user: newUser }; | ||
| this._currentUserCached = { isLoading: false, ...newV }; | ||
| this._dataForQueryCache = {}; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| export const createInstantRouteHandler = (config: { | ||
| appId: string; | ||
| apiURI?: string; | ||
| }) => { | ||
| async function handleUserSync(req: Request) { | ||
| const body = await req.json(); | ||
| if (body.user && body.user.refresh_token) { | ||
| return new Response('sync', { | ||
| headers: { | ||
| // 24 hour expiry | ||
| 'Set-Cookie': `instant_refresh_token=${body.user.refresh_token}; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=86400`, | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What happens if you're testing on localhost? I would expect the |
||
| }, | ||
| }); | ||
| } else { | ||
| return new Response('sync', { | ||
| headers: { | ||
| // remove the cookie (some browsers) | ||
| 'Set-Cookie': `instant_refresh_token=; Path=/; HttpOnly; Secure; SameSite=Strict; Max-Age=-1`, | ||
| }, | ||
| }); | ||
| } | ||
| } | ||
|
|
||
| return { | ||
| GET: async (_req: Request) => { | ||
| return new Response('Method not allowed', { | ||
| status: 405, | ||
| statusText: 'Method Not Allowed', | ||
| }); | ||
| }, | ||
| POST: async (req: Request) => { | ||
| const url = new URL(req.url); | ||
| const pathname = url.pathname; | ||
| const route = pathname.split('/')[pathname.split('/').length - 1]; | ||
| switch (route) { | ||
| case 'sync-auth': | ||
| return await handleUserSync(req); | ||
| } | ||
| return new Response('Route not found', { | ||
| status: 404, | ||
| }); | ||
| }, | ||
| }; | ||
| }; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since the cookie only lives for 24 hours, should we have something in reactor that periodically refreshes it?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I have it sync in the background when reactor gets init'd, not sure of a better solution? ideas welcome