From f40cf9559e33f6a6f17762dcc77eaf5d9c05beda Mon Sep 17 00:00:00 2001 From: David Dios Date: Mon, 20 Feb 2023 18:32:57 +0000 Subject: [PATCH 1/3] adding support for events, adding datasetLoaded and datasetChanged --- .gitignore | 2 ++ example/example.ts | 10 ++++-- example/index.html | 2 +- example/package.json | 4 +-- package.json | 1 + src/events.ts | 26 +++++++++++++++ src/groqStore.ts | 24 +++++++++++++- src/types.ts | 2 ++ test/events.test.ts | 79 ++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 143 insertions(+), 7 deletions(-) create mode 100644 src/events.ts create mode 100644 test/events.test.ts diff --git a/.gitignore b/.gitignore index cfdf407..3f24269 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,5 @@ node_modules /coverage *.iml .idea/ +.history +.parcel-cache diff --git a/example/example.ts b/example/example.ts index d15985c..009f065 100644 --- a/example/example.ts +++ b/example/example.ts @@ -13,13 +13,17 @@ import {groqStore, Subscription} from '../src/browser' populate() let subscription: Subscription | null | undefined - const dataset = groqStore({ + const store = groqStore({ projectId: 'groqstore', dataset: 'fixture', listen: true, overlayDrafts: true, }) + store.on('datasetLoaded', ({dataset, documents}) => { + console.info(`Dataset "${dataset}" loaded with ${documents.length} documents`) + }) + function attach() { clearBtnEl.addEventListener('click', clear, false) executeBtnEl.addEventListener('click', execute, false) @@ -39,7 +43,7 @@ import {groqStore, Subscription} from '../src/browser' resultEl.value = '… querying …' localStorage.setItem('groqStore', queryEl.value) try { - onResult(await dataset.query(queryEl.value)) + onResult(await store.query(queryEl.value)) } catch (err: any) { onError(err.message || 'Unknown error') } @@ -56,7 +60,7 @@ import {groqStore, Subscription} from '../src/browser' resultEl.value = '… querying …' executeBtnEl.disabled = true subscribeBtnEl.textContent = 'Unsubscribe' - subscription = dataset.subscribe(queryEl.value, {}, onResult) + subscription = store.subscribe(queryEl.value, {}, onResult) } } diff --git a/example/index.html b/example/index.html index ac35119..cf309e6 100644 --- a/example/index.html +++ b/example/index.html @@ -25,6 +25,6 @@

Result

- + diff --git a/example/package.json b/example/package.json index 5f81ce4..fd4496e 100644 --- a/example/package.json +++ b/example/package.json @@ -4,11 +4,11 @@ "private": true, "description": "", "license": "MIT", - "main": "src/example.ts", "scripts": { "start": "parcel index.html" }, "devDependencies": { "parcel-bundler": "^1.12.5" - } + }, + "default": "example.ts" } diff --git a/package.json b/package.json index 08339a8..1fcbd44 100644 --- a/package.json +++ b/package.json @@ -58,6 +58,7 @@ "build": "pkg build --strict && pkg --strict", "lint": "eslint .", "prepublishOnly": "npm run build", + "prettify": "prettier --write .", "start": "cd example && npm start", "test": "tsdx test" }, diff --git a/src/events.ts b/src/events.ts new file mode 100644 index 0000000..2c071c2 --- /dev/null +++ b/src/events.ts @@ -0,0 +1,26 @@ +import {EventEmitter} from 'events' +import {SanityDocument} from '@sanity/types' + +type Events = { + /** + * Emitted after the dataset was loaded + */ + datasetLoaded: { + dataset: string + documents: SanityDocument[] + } + + /** + * Emitted each time when the dataset changes. This happens when the dataset + * is initialised, and after mutations were applied through listeners. + */ + datasetChanged: { + dataset: string + documents: SanityDocument[] + } +} + +export interface TypedEventEmitter extends EventEmitter { + on(s: K, listener: (v: Events[K]) => void): this + emit(eventName: K, params: Events[K]): boolean +} diff --git a/src/groqStore.ts b/src/groqStore.ts index 9e84401..88ed49a 100644 --- a/src/groqStore.ts +++ b/src/groqStore.ts @@ -5,8 +5,11 @@ import {SanityDocument} from '@sanity/types' import {parse, evaluate} from 'groq-js' import {Config, EnvImplementations, GroqSubscription, GroqStore, Subscription} from './types' import {getSyncingDataset} from './syncingDataset' +import {EventEmitter} from 'events' +import {TypedEventEmitter} from './events' export function groqStore(config: Config, envImplementations: EnvImplementations): GroqStore { + const eventEmitter = new EventEmitter() as TypedEventEmitter let documents: SanityDocument[] = [] const executeThrottled = throttle(config.subscriptionThrottleMs || 50, executeAllSubscriptions) const activeSubscriptions: GroqSubscription[] = [] @@ -20,9 +23,20 @@ export function groqStore(config: Config, envImplementations: EnvImplementations (docs) => { documents = docs executeThrottled() + eventEmitter.emit('datasetChanged', { + dataset: config.dataset, + documents: docs, + }) }, envImplementations ) + + dataset.loaded.then(() => { + eventEmitter.emit('datasetLoaded', { + dataset: config.dataset, + documents, + }) + }) } await dataset.loaded @@ -97,8 +111,16 @@ export function groqStore(config: Config, envImplementations: EnvImplementations function close() { executeThrottled.cancel() + eventEmitter.removeAllListeners() return dataset ? dataset.unsubscribe() : Promise.resolve() } - return {query, getDocument, getDocuments, subscribe, close} + return { + query, + getDocument, + getDocuments, + subscribe, + close, + on: (...args) => eventEmitter.on(...args), + } } diff --git a/src/types.ts b/src/types.ts index 8c9a942..4ab5cec 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,5 +1,6 @@ import {SanityDocument} from '@sanity/types' import EventSourcePolyfill from 'eventsource' +import {TypedEventEmitter} from './events' /** @public */ export interface Subscription { @@ -89,6 +90,7 @@ export interface GroqStore { callback: (err: Error | undefined, result?: R) => void ) => Subscription close: () => Promise + on: TypedEventEmitter['on'] } export interface ApiError { diff --git a/test/events.test.ts b/test/events.test.ts new file mode 100644 index 0000000..361e80d --- /dev/null +++ b/test/events.test.ts @@ -0,0 +1,79 @@ +import EventSource from 'eventsource' +import {groqStore as groqStoreApi} from '../src/groqStore' +import {EnvImplementations, Config} from '../src/types' +import * as baseConfig from './config' +import * as listener from '../src/listen' +import {MutationEvent} from '@sanity/client' + +describe('events', () => { + it('datasetLoaded fires when dataset is loaded', async () => { + const config: Config = { + ...baseConfig, + token: 'my-token', + } + + const getDocuments = jest.fn().mockResolvedValue([{_id: 'foo', value: 'bar'}]) + const datasetLoadedCb = jest.fn() + + const store = groqStoreApi(config, { + EventSource: EventSource as any as EnvImplementations['EventSource'], + getDocuments, + }) + + store.on('datasetLoaded', datasetLoadedCb) + + await store.query('*') + + expect(datasetLoadedCb).toBeCalledTimes(1) + expect(datasetLoadedCb).toBeCalledWith({ + dataset: 'fixture', + documents: [{_id: 'foo', value: 'bar'}], + }) + }) + + it('datasetChanged fires each time the dataset changes', async () => { + const config: Config = { + ...baseConfig, + listen: true, + token: 'my-token', + } + + jest.useFakeTimers() + jest.spyOn(listener, 'listen').mockImplementation((_esImpl, _config, handlers) => { + handlers.open() + + // Call `next()` a little bit later to imitate a mutation received event + // eslint-disable-next-line max-nested-callbacks + setTimeout(() => handlers.next({} as any as MutationEvent), 50) + + return { + unsubscribe: () => Promise.resolve(), + } + }) + + const getDocuments = jest.fn().mockResolvedValue([ + {_id: 'foo', value: 'bar'}, + // {_id: 'bar', value: 'foo'}, + ]) + const datasetChangedCb = jest.fn() + + const store = groqStoreApi(config, { + EventSource: EventSource as any as EnvImplementations['EventSource'], + getDocuments, + }) + + store.on('datasetChanged', datasetChangedCb) + + await store.query('*') + + expect(datasetChangedCb).toBeCalledTimes(1) + expect(datasetChangedCb).toBeCalledWith({ + dataset: 'fixture', + documents: [{_id: 'foo', value: 'bar'}], + }) + + jest.advanceTimersByTime(100) + + expect(datasetChangedCb).toBeCalledTimes(2) + }) +}) From a6d9c38f5491b297df9d94c53fc6f2ec34a33178 Mon Sep 17 00:00:00 2001 From: David Dios Date: Mon, 20 Feb 2023 18:39:15 +0000 Subject: [PATCH 2/3] code cleanup --- example/example.ts | 1 + src/events.ts | 6 +++--- test/events.test.ts | 5 +---- 3 files changed, 5 insertions(+), 7 deletions(-) diff --git a/example/example.ts b/example/example.ts index 009f065..c595a04 100644 --- a/example/example.ts +++ b/example/example.ts @@ -21,6 +21,7 @@ import {groqStore, Subscription} from '../src/browser' }) store.on('datasetLoaded', ({dataset, documents}) => { + // eslint-disable-next-line no-console console.info(`Dataset "${dataset}" loaded with ${documents.length} documents`) }) diff --git a/src/events.ts b/src/events.ts index 2c071c2..cfcdc95 100644 --- a/src/events.ts +++ b/src/events.ts @@ -3,7 +3,7 @@ import {SanityDocument} from '@sanity/types' type Events = { /** - * Emitted after the dataset was loaded + * Emitted after the dataset was loaded. */ datasetLoaded: { dataset: string @@ -11,8 +11,8 @@ type Events = { } /** - * Emitted each time when the dataset changes. This happens when the dataset - * is initialised, and after mutations were applied through listeners. + * Emitted each time the dataset changes. This happens when the dataset + * is initialised, and after mutations are applied through listeners. */ datasetChanged: { dataset: string diff --git a/test/events.test.ts b/test/events.test.ts index 361e80d..e1a37fd 100644 --- a/test/events.test.ts +++ b/test/events.test.ts @@ -51,10 +51,7 @@ describe('events', () => { } }) - const getDocuments = jest.fn().mockResolvedValue([ - {_id: 'foo', value: 'bar'}, - // {_id: 'bar', value: 'foo'}, - ]) + const getDocuments = jest.fn().mockResolvedValue([{_id: 'foo', value: 'bar'}]) const datasetChangedCb = jest.fn() const store = groqStoreApi(config, { From 5f689b22b98b215f30fcd0ecbf9289b1a8804e94 Mon Sep 17 00:00:00 2001 From: Cody Olsen Date: Mon, 27 Feb 2023 16:56:08 +0100 Subject: [PATCH 3/3] chore(build): fix build errors --- src/events.ts | 4 +++- src/index.ts | 1 + 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/events.ts b/src/events.ts index cfcdc95..a3a4f88 100644 --- a/src/events.ts +++ b/src/events.ts @@ -1,7 +1,8 @@ import {EventEmitter} from 'events' import {SanityDocument} from '@sanity/types' -type Events = { +/** @public */ +export type Events = { /** * Emitted after the dataset was loaded. */ @@ -20,6 +21,7 @@ type Events = { } } +/** @public */ export interface TypedEventEmitter extends EventEmitter { on(s: K, listener: (v: Events[K]) => void): this emit(eventName: K, params: Events[K]): boolean diff --git a/src/index.ts b/src/index.ts index 25ee7ae..67c7055 100644 --- a/src/index.ts +++ b/src/index.ts @@ -19,3 +19,4 @@ export function groqStore(config: Config): GroqStore { export {default as groq} from 'groq' export type {Subscription, GroqStore, Config, EnvImplementations} from './types' +export type {Events, TypedEventEmitter} from './events'