diff --git a/.eslintrc.js b/.eslintrc.js index 9483b0a..8de734c 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -10,4 +10,14 @@ module.exports = { 'plugin:react-hooks/recommended', 'plugin:jest/recommended', ], + rules: { + '@typescript-eslint/ban-types': [ + 'error', + { + types: { + '{}': false, + }, + }, + ], + }, } diff --git a/src/context.test.tsx b/src/context.test.tsx index d2d1001..186916e 100644 --- a/src/context.test.tsx +++ b/src/context.test.tsx @@ -4,6 +4,7 @@ import PouchDB from 'pouchdb-core' import memory from 'pouchdb-adapter-memory' import { Provider, useContext } from './context' +import { sleep } from './test-utils' PouchDB.plugin(memory) @@ -53,9 +54,7 @@ test('should unsubscribe all when the database changes', async () => { rerender() - await new Promise(resolve => { - setTimeout(resolve, 10) - }) + await sleep(10) expect(unsubscribe).toHaveBeenCalled() @@ -81,9 +80,7 @@ test('should unsubscribe all when a database gets destroyed', async () => { await myPouch.destroy() - await new Promise(resolve => { - setTimeout(resolve, 10) - }) + await sleep(10) expect(unsubscribe).toHaveBeenCalled() }) diff --git a/src/global.d.ts b/src/global.d.ts index c7d82e9..0a34b8a 100644 --- a/src/global.d.ts +++ b/src/global.d.ts @@ -7,7 +7,7 @@ declare module 'pouchdb-utils' { } declare module 'pouchdb-selector-core' { - export function matchesSelector>( + export function matchesSelector( doc: PouchDB.Core.Document, selector: PouchDB.Find.Selector ): boolean diff --git a/src/subscription.test.ts b/src/subscription.test.ts index be06e89..b5dbd06 100644 --- a/src/subscription.test.ts +++ b/src/subscription.test.ts @@ -4,6 +4,8 @@ import mapReduce from 'pouchdb-mapreduce' import SubscriptionManager from './subscription' +import { sleep } from './test-utils' + PouchDB.plugin(memory) PouchDB.plugin(mapReduce) @@ -106,6 +108,55 @@ test('should only subscribe once to document updates', () => { expect(callback3).not.toHaveBeenCalled() }) +test('should handle unsubscribing during an doc update', () => { + const { changes, get } = myPouch + + const doc = { + _id: 'test', + _rev: '1-fb7e8b3df19087a905ab792366bd118a', + value: 42, + } + + let callback: ( + change: PouchDB.Core.ChangesResponseChange> + ) => void = () => { + console.error('should not be called') + } + + ;(myPouch as unknown as { changes: unknown }).changes = () => ({ + on(_type: string, callFn: (c: unknown) => void) { + callback = callFn + return { + cancel: jest.fn(), + } + }, + }) + + const subscriptionManager = new SubscriptionManager(myPouch) + const unsubscribe = subscriptionManager.subscribeToDocs(['test'], jest.fn()) + + let getCallback: (doc: unknown) => void = jest.fn() + ;(myPouch as unknown as { get: unknown }).get = jest.fn(() => { + return { + then(fn: (doc: unknown) => void) { + getCallback = fn + return { catch: jest.fn() } + }, + } + }) + + callback({ + id: 'test', + seq: 1, + changes: [{ rev: doc._rev }], + doc, + }) + unsubscribe() + myPouch.changes = changes + myPouch.get = get + expect(() => getCallback(doc)).not.toThrow() +}) + test('should subscribe to view updates', () => { const changesObject = { on: jest.fn(() => changesObject), @@ -227,9 +278,7 @@ test('should call the callback to documents with a document and to views with an value: 42, }) - await new Promise(resolve => { - setTimeout(resolve, 50) - }) + await sleep(50) expect(docCallback).toHaveBeenCalled() expect(typeof docCallback.mock.calls[0]).toBe('object') @@ -249,9 +298,7 @@ test('should call the callback to documents with a document and to views with an value: 'and the question is:', }) - await new Promise(resolve => { - setTimeout(resolve, 10) - }) + await sleep(10) expect(docCallback).toHaveBeenCalledTimes(1) expect(viewCallback).toHaveBeenCalledTimes(1) @@ -295,9 +342,7 @@ test('should have a unsubscribeAll method', async () => { value: 42, }) - await new Promise(resolve => { - setTimeout(resolve, 50) - }) + await sleep(50) expect(docCallback).not.toHaveBeenCalled() expect(allDocCallback).not.toHaveBeenCalled() @@ -346,9 +391,7 @@ test('should clone the documents that are passed to document callbacks', async ( value: 42, }) - await new Promise(resolve => { - setTimeout(resolve, 10) - }) + await sleep(10) ;(docs[0] as PouchDB.Core.IdMeta & { value: number }).value = 43 expect(docs).toHaveLength(2) @@ -375,9 +418,7 @@ test('should subscribe to all docs if null is passed to doc subscription', async } await myPouch.bulkDocs(docs) - await new Promise(resolve => { - setTimeout(resolve, 50) - }) + await sleep(50) expect(callback).toHaveBeenCalledTimes(15) diff --git a/src/subscription.ts b/src/subscription.ts index bc0a140..35455fc 100644 --- a/src/subscription.ts +++ b/src/subscription.ts @@ -1,15 +1,15 @@ import { clone } from 'pouchdb-utils' -export type DocsCallback> = ( +export type DocsCallback = ( deleted: boolean, id: PouchDB.Core.DocumentId, doc?: PouchDB.Core.Document ) => void interface DocsSubscription { - changesFeed: PouchDB.Core.Changes> - all: Set>> - ids: Map>>> + changesFeed: PouchDB.Core.Changes<{}> + all: Set> + ids: Map>> } export type ViewCallback = (id: PouchDB.Core.DocumentId) => void @@ -19,10 +19,10 @@ export type subscribeToView = ( ) => () => void interface SubscriptionToAView { - feed: PouchDB.Core.Changes> + feed: PouchDB.Core.Changes<{}> callbacks: Set } -export type subscribeToDocs = >( +export type subscribeToDocs = ( ids: PouchDB.Core.DocumentId[] | null, callback: DocsCallback ) => () => void @@ -44,7 +44,7 @@ export default class SubscriptionManager { pouch.once('destroyed', this.#destroyListener) } - subscribeToDocs>( + subscribeToDocs( ids: PouchDB.Core.DocumentId[] | null, callback: DocsCallback ): () => void { @@ -63,19 +63,15 @@ export default class SubscriptionManager { if (isIds) { for (const id of ids ?? []) { if (this.#docsSubscription.ids.has(id)) { - this.#docsSubscription.ids - .get(id) - ?.add(callback as DocsCallback>) + this.#docsSubscription.ids.get(id)?.add(callback as DocsCallback<{}>) } else { - const set: Set>> = new Set() - set.add(callback as DocsCallback>) + const set: Set> = new Set() + set.add(callback as DocsCallback<{}>) this.#docsSubscription.ids.set(id, set) } } } else { - this.#docsSubscription.all.add( - callback as DocsCallback> - ) + this.#docsSubscription.all.add(callback as DocsCallback<{}>) } let didUnsubscribe = false @@ -86,16 +82,14 @@ export default class SubscriptionManager { if (isIds) { for (const id of ids ?? []) { const set = this.#docsSubscription?.ids.get(id) - set?.delete(callback as DocsCallback>) + set?.delete(callback as DocsCallback<{}>) if (set?.size === 0) { this.#docsSubscription?.ids.delete(id) } } } else { - this.#docsSubscription?.all.delete( - callback as DocsCallback> - ) + this.#docsSubscription?.all.delete(callback as DocsCallback<{}>) } if ( @@ -172,47 +166,34 @@ function createDocSubscription(pouch: PouchDB.Database): DocsSubscription { live: true, }) .on('change', change => { - const hasAll = - docsSubscription?.all != null && docsSubscription.all.size > 0 - const hasId = docsSubscription && docsSubscription.ids.has(change.id) + const hasAll = (docsSubscription?.all.size ?? 0) > 0 + const idSubscriptions = docsSubscription?.ids.get(change.id) if (change.deleted) { - if (hasAll) { - const subscription = docsSubscription as DocsSubscription - notify(subscription.all, true, change.id) + if (hasAll && docsSubscription) { + notify(docsSubscription.all, true, change.id) } - if (hasId) { - const subscription = docsSubscription as DocsSubscription - notify( - subscription.ids.get(change.id) as Set< - DocsCallback> - >, - true, - change.id - ) + if (idSubscriptions) { + notify(idSubscriptions, true, change.id) } } else { pouch .get(change.id) .then(doc => { - if (hasAll) { - const subscription = docsSubscription as DocsSubscription + if (hasAll && docsSubscription) { notify( - subscription.all, + docsSubscription.all, false, change.id, - doc as unknown as PouchDB.Core.Document> + doc as unknown as PouchDB.Core.Document<{}> ) } - if (hasId) { - const subscription = docsSubscription as DocsSubscription + if (idSubscriptions) { notify( - subscription.ids.get(change.id) as Set< - DocsCallback> - >, + idSubscriptions, false, change.id, - doc as unknown as PouchDB.Core.Document> + doc as unknown as PouchDB.Core.Document<{}> ) } }) @@ -230,10 +211,10 @@ function createDocSubscription(pouch: PouchDB.Database): DocsSubscription { } function notify( - set: Set>>, + set: Set>, deleted: boolean, id: PouchDB.Core.DocumentId, - doc?: PouchDB.Core.Document> + doc?: PouchDB.Core.Document<{}> ) { for (const subscription of set) { try { diff --git a/src/test-utils.tsx b/src/test-utils.tsx index d032de2..48781ea 100644 --- a/src/test-utils.tsx +++ b/src/test-utils.tsx @@ -87,3 +87,9 @@ export async function waitForLoadingChange( expect(result.current.loading).toBe(desiredState) }) } + +export async function sleep(milliseconds: number): Promise { + return new Promise(resolve => { + setTimeout(resolve, milliseconds) + }) +} diff --git a/src/useAllDocs.test.ts b/src/useAllDocs.test.ts index 7c97c92..b7eb064 100644 --- a/src/useAllDocs.test.ts +++ b/src/useAllDocs.test.ts @@ -8,6 +8,7 @@ import { act, waitForNextUpdate, DocWithAttachment, + sleep, } from './test-utils' import useAllDocs from './useAllDocs' @@ -426,9 +427,7 @@ describe('options', () => { }) }) - await new Promise(resolve => { - setTimeout(resolve, 10) - }) + await sleep(10) expect(result.current.rows).toEqual([ { id: 'b', key: 'b', value: { rev: revB } }, @@ -481,9 +480,7 @@ describe('options', () => { myPouch.put({ _id: 'c', test: 'moar' }) }) - await new Promise(resolve => { - setTimeout(resolve, 10) - }) + await sleep(10) expect(result.current.rows).toEqual([ { id: 'a', key: 'a', value: { rev: revA } }, @@ -517,9 +514,7 @@ describe('options', () => { myPouch.put({ _id: 'c', test: 'moar' }) }) - await new Promise(resolve => { - setTimeout(resolve, 10) - }) + await sleep(10) expect(result.current.rows).toEqual([ { id: 'a', key: 'a', value: { rev: revA } }, @@ -712,9 +707,7 @@ describe('options', () => { myPouch.put({ _id: 'c', test: 'moar' }) }) - await new Promise(resolve => { - setTimeout(resolve, 10) - }) + await sleep(10) expect(result.current.state).toBe('done') expect(result.current.rows).toEqual([ @@ -759,9 +752,7 @@ describe('options', () => { myPouch.put({ _id: 'd', test: 'moar' }) }) - await new Promise(resolve => { - setTimeout(resolve, 10) - }) + await sleep(10) expect(result.current.state).toBe('done') expect(result.current.rows).toEqual([ diff --git a/src/useAllDocs.ts b/src/useAllDocs.ts index 9aafc78..7b29944 100644 --- a/src/useAllDocs.ts +++ b/src/useAllDocs.ts @@ -8,7 +8,7 @@ import { useDeepMemo, CommonOptions } from './utils' * Get all docs or a slice of all docs and subscribe to their updates. * @param options PouchDB's allDocs options. */ -export default function useAllDocs>( +export default function useAllDocs( options?: CommonOptions & ( | PouchDB.Core.AllDocsWithKeyOptions diff --git a/src/useDoc.test.ts b/src/useDoc.test.ts index 298f464..7c9b34d 100644 --- a/src/useDoc.test.ts +++ b/src/useDoc.test.ts @@ -7,6 +7,7 @@ import { act, waitForNextUpdate, DocWithAttachment, + sleep, } from './test-utils' import useDoc from './useDoc' @@ -479,9 +480,7 @@ describe('pouchdb get options', () => { value: 'update', }) - await new Promise(resolve => { - setTimeout(resolve, 10) - }) + await sleep(10) expect(result.current.state).toBe('done') expect(result.current.doc).toEqual({ diff --git a/src/useDoc.ts b/src/useDoc.ts index e87b5ae..06d6f99 100644 --- a/src/useDoc.ts +++ b/src/useDoc.ts @@ -4,7 +4,7 @@ import { useContext } from './context' import useStateMachine, { ResultType } from './state-machine' import type { CommonOptions } from './utils' -type DocResultType> = ResultType<{ +type DocResultType = ResultType<{ doc: (PouchDB.Core.Document & PouchDB.Core.GetMeta) | null }> @@ -14,7 +14,7 @@ type DocResultType> = ResultType<{ * @param {object} [options] - PouchDB get options. Excluding 'open_revs'. * @param {object|function} [initialValue] - Value that should be returned while fetching the doc. */ -export default function useDoc>( +export default function useDoc( id: PouchDB.Core.DocumentId, options?: (PouchDB.Core.GetOptions & CommonOptions) | null, initialValue?: (() => Content) | Content diff --git a/src/useFind.test.ts b/src/useFind.test.ts index a3822d1..286e945 100644 --- a/src/useFind.test.ts +++ b/src/useFind.test.ts @@ -8,6 +8,7 @@ import { waitForNextUpdate, waitForLoadingChange, act, + sleep, } from './test-utils' import useFind, { FindHookIndexOption } from './useFind' @@ -141,9 +142,7 @@ describe('by id', () => { }) }) - await new Promise(resolve => { - setTimeout(resolve, 10) - }) + await sleep(10) expect(result.current.loading).toBeFalsy() expect(result.current.docs).toHaveLength(5) @@ -732,9 +731,7 @@ describe('index', () => { }) }) - await new Promise(resolve => { - setTimeout(resolve, 20) - }) + await sleep(20) expect(result.current.error).toBeFalsy() expect(result.current.loading).toBeFalsy() expect(result.current.docs).toHaveLength(5) @@ -1131,9 +1128,7 @@ describe('index', () => { _id: 'aa', captain: 'Captain Hook', }) - return new Promise(resolve => { - setTimeout(resolve, 20) - }) + await sleep(20) }) expect(result.current.error).toBeFalsy() @@ -1146,9 +1141,7 @@ describe('index', () => { captain: 'Käpt’n Blaubär', aired: 1991, }) - return new Promise(resolve => { - setTimeout(resolve, 20) - }) + await sleep(20) }) await waitForLoadingChange(result, false) diff --git a/src/useFind.ts b/src/useFind.ts index 73a1005..6ed740c 100644 --- a/src/useFind.ts +++ b/src/useFind.ts @@ -61,7 +61,7 @@ export interface FindHookOptions extends CommonOptions { * Query, and optionally create, a Mango index and subscribe to its updates. * @param {object} [opts] A combination of PouchDB's find options and create index options. */ -export default function useFind>( +export default function useFind( options: FindHookOptions ): ResultType> { const { pouchdb: pouch, subscriptionManager } = useContext(options.db) @@ -236,7 +236,7 @@ export default function useFind>( function getIndex( db: PouchDB.Database, index: FindHookIndexOption | undefined, - selector: PouchDB.Find.FindRequest> + selector: PouchDB.Find.FindRequest<{}> ): Promise<[string | null, string]> { if (index && typeof index === 'string') { return findIndex(db, selector) @@ -260,7 +260,7 @@ async function createIndex( ): Promise<[string, string]> { const result = (await db.createIndex( index - )) as PouchDB.Find.CreateIndexResponse> & { + )) as PouchDB.Find.CreateIndexResponse<{}> & { id: PouchDB.Core.DocumentId name: string } @@ -274,7 +274,7 @@ async function createIndex( */ async function findIndex( db: PouchDB.Database, - selector: PouchDB.Find.FindRequest> + selector: PouchDB.Find.FindRequest<{}> ): Promise<[string | null, string]> { const database = db as PouchDB.Database & { explain: (selector: PouchDB.Find.Selector) => Promise @@ -303,22 +303,19 @@ function subscribe( ? '_design/' + id.replace(/^_design\//, '') // normalize, user can add a ddoc name : undefined - return subscriptionManager.subscribeToDocs>( - null, - (_del, id, doc) => { - if (idsInResult.ids.has(id)) { - query() - } else if (id === ddocName) { - query() - } else if (doc && typeof matchesSelector !== 'function') { - // because pouchdb-selector-core is semver-free zone - // If matchesSelector doesn't exist, just query every time - query() - } else if (doc && matchesSelector(doc, selector)) { - query() - } + return subscriptionManager.subscribeToDocs<{}>(null, (_del, id, doc) => { + if (idsInResult.ids.has(id)) { + query() + } else if (id === ddocName) { + query() + } else if (doc && typeof matchesSelector !== 'function') { + // because pouchdb-selector-core is semver-free zone + // If matchesSelector doesn't exist, just query every time + query() + } else if (doc && matchesSelector(doc, selector)) { + query() } - ) + }) } interface ExplainResult { diff --git a/src/usePouch.ts b/src/usePouch.ts index 47457c0..e9a668b 100644 --- a/src/usePouch.ts +++ b/src/usePouch.ts @@ -4,7 +4,7 @@ import { useContext } from './context' * Get access to the PouchDB database that is provided by the provider. * @param {string | undefined} dbName Select the database to be returned by its name/key. */ -export default function usePouch>( +export default function usePouch( dbName?: string ): PouchDB.Database { return useContext(dbName).pouchdb as PouchDB.Database diff --git a/src/useView.test.ts b/src/useView.test.ts index 2264613..4900700 100644 --- a/src/useView.test.ts +++ b/src/useView.test.ts @@ -14,14 +14,9 @@ import useView from './useView' PouchDB.plugin(memory) PouchDB.plugin(mapReduce) -type TempView = PouchDB.Map< - PouchDB.Core.Document>, - unknown -> -type TempViewDoc = PouchDB.Filter< - PouchDB.Core.Document>, - unknown -> +type Doc = PouchDB.Core.Document<{ type: string; test: number; value?: number }> +type TempView = PouchDB.Map +type TempViewDoc = PouchDB.Filter let myPouch: PouchDB.Database @@ -975,7 +970,7 @@ describe('temporary function only views', () => { const view: TempView = (doc, emit) => { if (doc.type === 'tester') { - emit?.(doc.type, doc.value) + emit?.(doc.type, doc.value ?? 1) } } @@ -2235,7 +2230,7 @@ describe('temporary views objects', () => { const view: TempViewDoc = { map: (doc, emit) => { if (doc.type === 'tester') { - emit?.(doc.type, doc.value) + emit?.(doc.type, doc.value ?? 1) } }, } diff --git a/src/useView.ts b/src/useView.ts index 45ad9b2..385cd8a 100644 --- a/src/useView.ts +++ b/src/useView.ts @@ -6,18 +6,16 @@ import type SubscriptionManager from './subscription' import useStateMachine, { ResultType, Dispatch } from './state-machine' import { useDeepMemo, CommonOptions } from './utils' -type ViewResponseBase> = - PouchDB.Query.Response & { - /** - * Include an update_seq value indicating which sequence id of the underlying database the view - * reflects. - */ - update_seq?: number | string - } +/* typescript-eslint-disable @typescript-eslint/ban-types */ +type ViewResponseBase = PouchDB.Query.Response & { + /** + * Include an update_seq value indicating which sequence id of the underlying database the view + * reflects. + */ + update_seq?: number | string +} -export type ViewResponse> = ResultType< - ViewResponseBase -> +export type ViewResponse = ResultType> /** * Query a view and subscribe to its updates. @@ -25,9 +23,9 @@ export type ViewResponse> = ResultType< * @param {object} [opts] PouchDB's query-options */ export default function useView< - Content extends Record, - Result extends Record, - Model extends Record = Content + Content extends {}, + Result extends {}, + Model extends {} = Content >( fun: string | PouchDB.Map | PouchDB.Filter, opts?: PouchDB.Query.Options & {