diff --git a/.github/workflows/falcor-react-redux.yml b/.github/workflows/falcor-react-redux.yml new file mode 100644 index 00000000..5d9239d9 --- /dev/null +++ b/.github/workflows/falcor-react-redux.yml @@ -0,0 +1,39 @@ +name: Falcor React Redux query lifecycle + +on: + pull_request: + paths: + - packages/falcor-react-redux/** + - .github/workflows/falcor-react-redux.yml + push: + branches: [master] + paths: + - packages/falcor-react-redux/** + - .github/workflows/falcor-react-redux.yml + +permissions: + contents: read + +jobs: + query-lifecycle: + name: Query lifecycle (Node ${{ matrix.node }}) + runs-on: ubuntu-24.04 + timeout-minutes: 10 + strategy: + fail-fast: false + matrix: + node: [24, 26] + defaults: + run: + working-directory: packages/falcor-react-redux + steps: + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: ${{ matrix.node }} + - name: Install existing package dependencies and peers + run: npm install --ignore-scripts --no-package-lock --no-audit --no-fund + - name: Build package from source + run: npm run build + - name: Test actual query and disposal behavior + run: npm test diff --git a/packages/falcor-react-redux/lib/utils/fetchDataUntilSettled.js b/packages/falcor-react-redux/lib/utils/fetchDataUntilSettled.js index 89c323e9..773a7e7a 100644 --- a/packages/falcor-react-redux/lib/utils/fetchDataUntilSettled.js +++ b/packages/falcor-react-redux/lib/utils/fetchDataUntilSettled.js @@ -163,7 +163,7 @@ var DelayDisposeOperator = function () { (0, _createClass3.default)(DelayDisposeOperator, [{ key: 'call', value: function call(sink, source) { - return source._subscribe(new DelayDisposeSubscriber(sink, this.scheduler, this.delay)); + return source.subscribe(new DelayDisposeSubscriber(sink, this.scheduler, this.delay)); } }]); return DelayDisposeOperator; diff --git a/packages/falcor-react-redux/lib/utils/fetchDataUntilSettled.js.map b/packages/falcor-react-redux/lib/utils/fetchDataUntilSettled.js.map index e77e0a84..44557a00 100644 --- a/packages/falcor-react-redux/lib/utils/fetchDataUntilSettled.js.map +++ b/packages/falcor-react-redux/lib/utils/fetchDataUntilSettled.js.map @@ -1 +1 @@ -{"version":3,"sources":["../../src/utils/fetchDataUntilSettled.js"],"names":["fetchDataUntilSettled","memoizedQuerySyntax","data","query","props","falcor","version","fragment","disposeDelay","disposeScheduler","memo","loading","mapNext","handleNext","catchError","handleError","Observable","of","expand","_fetchDataUntilSettled","hydrateExistingData","skip","ast","error","from","withoutDataSource","get","map","catch","empty","nextQuery","e","$__status","console","getVersion","progressively","let","delayDispose","bind","json","scheduler","delay","source","lift","DelayDisposeOperator","sink","_subscribe","DelayDisposeSubscriber","unsubscriptionDisposable","closed","isStopped","schedule","unsubscribe","err","destination","complete","Subscriber"],"mappings":"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","file":"fetchDataUntilSettled.js","sourcesContent":["import { errorMessage } from 'pegjs-util';\nimport memoizeQueryies from './memoizeQueryies';\nimport { Observable } from 'rxjs/Observable';\nimport { Subscriber } from 'rxjs/Subscriber';\nimport 'rxjs/add/operator/map';\nimport 'rxjs/add/operator/catch';\nimport 'rxjs/add/operator/expand';\nimport 'rxjs/add/observable/of';\nimport 'rxjs/add/observable/from';\nimport 'rxjs/add/observable/empty';\n\nconst memoizedQuerySyntax = memoizeQueryies(100);\n\nexport default function fetchDataUntilSettled({\n data, query, props, falcor, version, fragment,\n disposeDelay = 0, disposeScheduler\n}) {\n\n const memo = {\n query, loading: true,\n data, props, falcor, version, fragment,\n disposeDelay, disposeScheduler\n };\n memo.mapNext = handleNext(memo, falcor);\n memo.catchError = handleError(memo, falcor);\n if (!memo.data) {\n return Observable.of(memo).expand(_fetchDataUntilSettled);\n }\n return hydrateExistingData(memo).expand(_fetchDataUntilSettled).skip(1);\n}\n\nfunction hydrateExistingData(memo) {\n if (memo.query) {\n const { ast, error } = memoizedQuerySyntax(memo.query);\n if (!error) {\n return Observable\n .from(memo.falcor.withoutDataSource().get(ast))\n .map(memo.mapNext).catch(memo.catchError);\n }\n }\n return Observable.of(memo);\n}\n\nfunction _fetchDataUntilSettled(memo) {\n if (memo.loading === false) {\n return Observable.empty();\n }\n let nextQuery;\n const { query, falcor, fragment } = memo;\n try {\n nextQuery = fragment(memo.data || {}, memo.props || {});\n } catch (e) {\n return memo.catchError(e);\n }\n if (query !== (memo.query = nextQuery) || (memo.data && memo.data.$__status === 'incomplete')) {\n const { ast, error } = memoizedQuerySyntax(nextQuery);\n if (error) {\n if (typeof console !== 'undefined' && typeof console.error === 'function') {\n console.error(errorMessage(error));\n console.error(`Error parsing query: ${nextQuery}`);\n }\n memo.error = error;\n memo.version = falcor.getVersion();\n } else {\n return Observable\n .from(falcor.get(ast).progressively())\n .map(memo.mapNext).catch(memo.catchError)\n .let(delayDispose.bind(null, memo.disposeScheduler, memo.disposeDelay));\n }\n }\n memo.loading = false;\n return Observable.of(memo);\n}\n\nfunction handleNext(memo, falcor) {\n return function mapNext({ json: data }) {\n memo.data = data;\n memo.loading = true;\n memo.version = falcor.getVersion();\n return memo;\n }\n}\n\nfunction handleError(memo, falcor) {\n return function catchError(error) {\n memo.error = error;\n memo.loading = false;\n memo.version = falcor.getVersion();\n return Observable.of(memo);\n };\n}\n\nfunction delayDispose(scheduler, delay, source) {\n return !scheduler ? source : source.lift(new DelayDisposeOperator(scheduler, delay));\n}\n\nclass DelayDisposeOperator {\n constructor(scheduler, delay) {\n this.delay = delay;\n this.scheduler = scheduler;\n }\n call(sink, source) {\n return source._subscribe(new DelayDisposeSubscriber(sink, this.scheduler, this.delay));\n }\n}\n\nclass DelayDisposeSubscriber extends Subscriber {\n constructor(sink, scheduler, delay) {\n super(sink);\n this.delay = delay;\n this.scheduler = scheduler;\n this.unsubscriptionDisposable = null;\n }\n superUnsubscribe() { return super.unsubscribe(); }\n unsubscribe() {\n if (!this.closed && !this.isStopped && !this.unsubscriptionDisposable) {\n this.isStopped = true;\n this.unsubscriptionDisposable = this.scheduler.schedule(() => {\n super.unsubscribe();\n }, this.delay);\n }\n }\n _unsubscribe() {\n const { unsubscriptionDisposable } = this;\n if (unsubscriptionDisposable) {\n this.unsubscriptionDisposable = null;\n unsubscriptionDisposable.unsubscribe();\n }\n }\n _error(err) {\n this.destination.error(err);\n super.unsubscribe();\n }\n _complete() {\n this.destination.complete();\n super.unsubscribe();\n }\n}\n"]} \ No newline at end of file +{"version":3,"sources":["../../src/utils/fetchDataUntilSettled.js"],"names":["fetchDataUntilSettled","memoizedQuerySyntax","data","query","props","falcor","version","fragment","disposeDelay","disposeScheduler","memo","loading","mapNext","handleNext","catchError","handleError","Observable","of","expand","_fetchDataUntilSettled","hydrateExistingData","skip","ast","error","from","withoutDataSource","get","map","catch","empty","nextQuery","e","$__status","console","getVersion","progressively","let","delayDispose","bind","json","scheduler","delay","source","lift","DelayDisposeOperator","sink","subscribe","DelayDisposeSubscriber","unsubscriptionDisposable","closed","isStopped","schedule","unsubscribe","err","destination","complete","Subscriber"],"mappings":"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","file":"fetchDataUntilSettled.js","sourcesContent":["import { errorMessage } from 'pegjs-util';\nimport memoizeQueryies from './memoizeQueryies';\nimport { Observable } from 'rxjs/Observable';\nimport { Subscriber } from 'rxjs/Subscriber';\nimport 'rxjs/add/operator/map';\nimport 'rxjs/add/operator/catch';\nimport 'rxjs/add/operator/expand';\nimport 'rxjs/add/observable/of';\nimport 'rxjs/add/observable/from';\nimport 'rxjs/add/observable/empty';\n\nconst memoizedQuerySyntax = memoizeQueryies(100);\n\nexport default function fetchDataUntilSettled({\n data, query, props, falcor, version, fragment,\n disposeDelay = 0, disposeScheduler\n}) {\n\n const memo = {\n query, loading: true,\n data, props, falcor, version, fragment,\n disposeDelay, disposeScheduler\n };\n memo.mapNext = handleNext(memo, falcor);\n memo.catchError = handleError(memo, falcor);\n if (!memo.data) {\n return Observable.of(memo).expand(_fetchDataUntilSettled);\n }\n return hydrateExistingData(memo).expand(_fetchDataUntilSettled).skip(1);\n}\n\nfunction hydrateExistingData(memo) {\n if (memo.query) {\n const { ast, error } = memoizedQuerySyntax(memo.query);\n if (!error) {\n return Observable\n .from(memo.falcor.withoutDataSource().get(ast))\n .map(memo.mapNext).catch(memo.catchError);\n }\n }\n return Observable.of(memo);\n}\n\nfunction _fetchDataUntilSettled(memo) {\n if (memo.loading === false) {\n return Observable.empty();\n }\n let nextQuery;\n const { query, falcor, fragment } = memo;\n try {\n nextQuery = fragment(memo.data || {}, memo.props || {});\n } catch (e) {\n return memo.catchError(e);\n }\n if (query !== (memo.query = nextQuery) || (memo.data && memo.data.$__status === 'incomplete')) {\n const { ast, error } = memoizedQuerySyntax(nextQuery);\n if (error) {\n if (typeof console !== 'undefined' && typeof console.error === 'function') {\n console.error(errorMessage(error));\n console.error(`Error parsing query: ${nextQuery}`);\n }\n memo.error = error;\n memo.version = falcor.getVersion();\n } else {\n return Observable\n .from(falcor.get(ast).progressively())\n .map(memo.mapNext).catch(memo.catchError)\n .let(delayDispose.bind(null, memo.disposeScheduler, memo.disposeDelay));\n }\n }\n memo.loading = false;\n return Observable.of(memo);\n}\n\nfunction handleNext(memo, falcor) {\n return function mapNext({ json: data }) {\n memo.data = data;\n memo.loading = true;\n memo.version = falcor.getVersion();\n return memo;\n }\n}\n\nfunction handleError(memo, falcor) {\n return function catchError(error) {\n memo.error = error;\n memo.loading = false;\n memo.version = falcor.getVersion();\n return Observable.of(memo);\n };\n}\n\nfunction delayDispose(scheduler, delay, source) {\n return !scheduler ? source : source.lift(new DelayDisposeOperator(scheduler, delay));\n}\n\nclass DelayDisposeOperator {\n constructor(scheduler, delay) {\n this.delay = delay;\n this.scheduler = scheduler;\n }\n call(sink, source) {\n return source.subscribe(new DelayDisposeSubscriber(sink, this.scheduler, this.delay));\n }\n}\n\nclass DelayDisposeSubscriber extends Subscriber {\n constructor(sink, scheduler, delay) {\n super(sink);\n this.delay = delay;\n this.scheduler = scheduler;\n this.unsubscriptionDisposable = null;\n }\n superUnsubscribe() { return super.unsubscribe(); }\n unsubscribe() {\n if (!this.closed && !this.isStopped && !this.unsubscriptionDisposable) {\n this.isStopped = true;\n this.unsubscriptionDisposable = this.scheduler.schedule(() => {\n super.unsubscribe();\n }, this.delay);\n }\n }\n _unsubscribe() {\n const { unsubscriptionDisposable } = this;\n if (unsubscriptionDisposable) {\n this.unsubscriptionDisposable = null;\n unsubscriptionDisposable.unsubscribe();\n }\n }\n _error(err) {\n this.destination.error(err);\n super.unsubscribe();\n }\n _complete() {\n this.destination.complete();\n super.unsubscribe();\n }\n}\n"]} \ No newline at end of file diff --git a/packages/falcor-react-redux/package.json b/packages/falcor-react-redux/package.json index c40ca20b..87b9c74a 100644 --- a/packages/falcor-react-redux/package.json +++ b/packages/falcor-react-redux/package.json @@ -9,7 +9,8 @@ "clean": "rimraf lib && mkdirp lib", "build": "npm run clean && babel src -d lib --source-maps", "watch": "npm run clean && babel src -d lib --source-maps --watch", - "prepublish": "npm run build" + "prepublish": "npm run build", + "test": "node test/fetchDataUntilSettled.test.cjs" }, "repository": { "type": "git", diff --git a/packages/falcor-react-redux/src/utils/fetchDataUntilSettled.js b/packages/falcor-react-redux/src/utils/fetchDataUntilSettled.js index aa6c47db..b6ff7e8a 100644 --- a/packages/falcor-react-redux/src/utils/fetchDataUntilSettled.js +++ b/packages/falcor-react-redux/src/utils/fetchDataUntilSettled.js @@ -100,7 +100,7 @@ class DelayDisposeOperator { this.scheduler = scheduler; } call(sink, source) { - return source._subscribe(new DelayDisposeSubscriber(sink, this.scheduler, this.delay)); + return source.subscribe(new DelayDisposeSubscriber(sink, this.scheduler, this.delay)); } } diff --git a/packages/falcor-react-redux/test/fetchDataUntilSettled.test.cjs b/packages/falcor-react-redux/test/fetchDataUntilSettled.test.cjs new file mode 100644 index 00000000..0a1dabde --- /dev/null +++ b/packages/falcor-react-redux/test/fetchDataUntilSettled.test.cjs @@ -0,0 +1,90 @@ +'use strict'; +const test = require('node:test'); +const assert = require('node:assert/strict'); +const Rx = require('rxjs'); +const fetchDataUntilSettled = require('../lib/utils/fetchDataUntilSettled').default; + +function fixture(options = {}) { + const read = new Rx.Subject(); + const scheduler = new Rx.VirtualTimeScheduler(); + const values = [], errors = []; + let calls = 0; + const falcor = { + get() { calls++; return { progressively: () => read }; }, + getVersion: () => 1 + }; + const query = fetchDataUntilSettled({ + falcor, fragment: () => '{ value }', + disposeScheduler: scheduler, disposeDelay: 5, + ...options + }); + return { read, scheduler, values, errors, query, calls: () => calls }; +} + +test('a delayed query maps an asynchronous read error into a settled error memo', () => { + const f = fixture(), failure = { $type: 'error', value: 'retry exhausted' }; + const sub = f.query.subscribe(v => f.values.push(v), e => f.errors.push(e)); + f.read.error(failure); + assert.deepEqual(f.errors, []); + assert.equal(f.values.at(-1).error, failure); + assert.equal(f.values.at(-1).loading, false); + assert.equal(f.calls(), 1); + sub.unsubscribe(); f.scheduler.flush(); +}); + +test('an exhausted read does not throw through a next-only component subscription', () => { + const f = fixture(), failure = new Error('retry exhausted'); + const sub = f.query.subscribe(v => f.values.push(v)); + assert.doesNotThrow(() => f.read.error(failure)); + assert.equal(f.values.at(-1).error, failure); + assert.equal(f.calls(), 1); + sub.unsubscribe(); f.scheduler.flush(); +}); + +test('without a disposal scheduler the existing query error handler still runs', () => { + const f = fixture({ disposeScheduler: null }), failure = new Error('read failed'); + const sub = f.query.subscribe(v => f.values.push(v), e => f.errors.push(e)); + f.read.error(failure); + assert.deepEqual(f.errors, []); + assert.equal(f.values.at(-1).error, failure); + assert.equal(f.values.at(-1).loading, false); + sub.unsubscribe(); +}); + +test('a successful query maps actual data without an extra read', () => { + const f = fixture(); + const sub = f.query.subscribe(v => f.values.push(v), e => f.errors.push(e)); + f.read.next({ json: { value: 57 } }); + f.read.complete(); + assert.deepEqual(f.errors, []); + assert.deepEqual(f.values.at(-1).data, { value: 57 }); + assert.equal(f.calls(), 1); + sub.unsubscribe(); f.scheduler.flush(); +}); + +test('disposal stays delayed while late values never reach a disposed component', () => { + const f = fixture(); + const sub = f.query.subscribe(v => f.values.push(v), e => f.errors.push(e)); + assert.equal(f.read.observers.length, 1); + const count = f.values.length; + sub.unsubscribe(); + assert.equal(f.read.observers.length, 1, 'upstream disposal was not delayed'); + f.read.next({ json: { value: 99 } }); + assert.equal(f.values.length, count); + f.scheduler.flush(); + assert.equal(f.read.observers.length, 0); + assert.equal(f.calls(), 1); +}); + +test('a synchronous completed read settles and disposes without resubscribing', () => { + let calls = 0, values = [], errors = []; + const falcor = { get() { calls++; return { progressively: () => Rx.Observable.of({ json: { value: 57 } }) }; }, getVersion: () => 1 }; + const scheduler = new Rx.VirtualTimeScheduler(); + const sub = fetchDataUntilSettled({ falcor, fragment: () => '{ value }', disposeScheduler: scheduler, disposeDelay: 5 }) + .subscribe(v => values.push(v), e => errors.push(e)); + assert.deepEqual(errors, []); + assert.deepEqual(values.at(-1).data, { value: 57 }); + assert.equal(values.at(-1).loading, false); + assert.equal(calls, 1); + sub.unsubscribe(); scheduler.flush(); +});