mirror of
https://github.com/re-rxjs/react-rxjs.git
synced 2025-12-08 18:01:51 +00:00
monorepo: @react-rxjs/core @react-rxjs/utils @react-rxjs/dom
This commit is contained in:
parent
1414c417ff
commit
431d56e16e
@ -1,5 +1,5 @@
|
||||
language: node_js
|
||||
node_js: node
|
||||
cache: npm
|
||||
cache: yarn
|
||||
script:
|
||||
- npm run test:ci
|
||||
- yarn test:ci
|
||||
|
||||
10588
package-lock.json
generated
10588
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
62
package.json
62
package.json
@ -1,65 +1,23 @@
|
||||
{
|
||||
"version": "3.0.0-alpha.5",
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "./dist/react-rxjs.cjs.production.min.js",
|
||||
"maxSize": "6 kB",
|
||||
"compression": "none"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/re-rxjs/react-rxjs.git"
|
||||
},
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "tsdx watch",
|
||||
"build": "tsdx build",
|
||||
"coverage": "codecov",
|
||||
"test": "tsdx test --passWithNoTests --coverage",
|
||||
"test:ci": "npm run test && npm run coverage && bundlesize",
|
||||
"lint": "tsdx lint",
|
||||
"prepare": "tsdx build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"rxjs": ">=6"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "tsdx lint"
|
||||
}
|
||||
"build": "wsrun build",
|
||||
"lint": "wsrun lint",
|
||||
"test:ci": "cross-env CI=true wsrun test:ci"
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 80,
|
||||
"semi": false,
|
||||
"trailingComma": "all"
|
||||
},
|
||||
"name": "react-rxjs",
|
||||
"author": "Josep M Sobrepere",
|
||||
"module": "dist/react-rxjs.esm.js",
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^10.4.6",
|
||||
"@testing-library/react-hooks": "^3.3.0",
|
||||
"@types/jest": "^26.0.4",
|
||||
"@types/react": "^16.9.43",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"bundlesize": "^0.18.0",
|
||||
"codecov": "^3.7.0",
|
||||
"husky": "^4.2.5",
|
||||
"jest-marbles": "^2.5.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
"rxjs": "^6.6.0",
|
||||
"tsdx": "^0.13.2",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.6"
|
||||
"cross-env": "^7.0.2",
|
||||
"prettier": "^2.0.5",
|
||||
"typescript": "^3.9.6",
|
||||
"wsrun": "^5.2.1"
|
||||
}
|
||||
}
|
||||
|
||||
65
packages/core/package.json
Normal file
65
packages/core/package.json
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"version": "0.1.0-alpha.0",
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "./dist/react-rxjs/core.cjs.production.min.js",
|
||||
"maxSize": "5 kB",
|
||||
"compression": "none"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/re-rxjs/react-rxjs.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "tsdx watch",
|
||||
"build": "tsdx build",
|
||||
"coverage": "codecov",
|
||||
"test": "tsdx test --passWithNoTests --coverage",
|
||||
"test:ci": "npm run test && npm run coverage && bundlesize",
|
||||
"lint": "tsdx lint",
|
||||
"prepare": "tsdx build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"rxjs": ">=6"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "tsdx lint"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 80,
|
||||
"semi": false,
|
||||
"trailingComma": "all"
|
||||
},
|
||||
"name": "@react-rxjs/core",
|
||||
"author": "Josep M Sobrepere",
|
||||
"module": "dist/react-rxjs/core.esm.js",
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^10.4.6",
|
||||
"@testing-library/react-hooks": "^3.3.0",
|
||||
"@types/jest": "^26.0.4",
|
||||
"@types/react": "^16.9.43",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"bundlesize": "^0.18.0",
|
||||
"codecov": "^3.7.0",
|
||||
"husky": "^4.2.5",
|
||||
"jest-marbles": "^2.5.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
"rxjs": "^6.6.0",
|
||||
"tsdx": "^0.13.2",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.6"
|
||||
}
|
||||
}
|
||||
@ -1,15 +1,10 @@
|
||||
// core
|
||||
export { connectObservable } from "./connectObservable"
|
||||
export { connectFactoryObservable } from "./connectFactoryObservable"
|
||||
export { shareLatest } from "./operators/shareLatest"
|
||||
|
||||
// support for React Suspense
|
||||
export { SUSPENSE } from "./SUSPENSE"
|
||||
export { suspend } from "./operators/suspend"
|
||||
export { suspended } from "./operators/suspended"
|
||||
export { switchMapSuspended } from "./operators/switchMapSuspended"
|
||||
|
||||
// utils
|
||||
export { shareLatest } from "./operators/shareLatest"
|
||||
export { useSubscribe } from "./useSubscribe"
|
||||
export { Subscribe } from "./Subscribe"
|
||||
export { subjectFactory } from "./subjectFactory"
|
||||
7
packages/dom/jest.config.js
Normal file
7
packages/dom/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
diagnostics: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
66
packages/dom/package.json
Normal file
66
packages/dom/package.json
Normal file
@ -0,0 +1,66 @@
|
||||
{
|
||||
"version": "0.1.0-alpha.0",
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "./dist/react-rxjs/dom.cjs.production.min.js",
|
||||
"maxSize": "1 kB",
|
||||
"compression": "none"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/re-rxjs/react-rxjs.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "tsdx watch",
|
||||
"build": "tsdx build",
|
||||
"coverage": "codecov",
|
||||
"test": "tsdx test --passWithNoTests --coverage",
|
||||
"test:ci": "npm run test && npm run coverage && bundlesize",
|
||||
"lint": "tsdx lint",
|
||||
"prepare": "tsdx build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"react-dom": ">=16.8.0",
|
||||
"rxjs": ">=6"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "tsdx lint"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 80,
|
||||
"semi": false,
|
||||
"trailingComma": "all"
|
||||
},
|
||||
"name": "@react-rxjs/dom",
|
||||
"author": "Josep M Sobrepere",
|
||||
"module": "dist/react-rxjs/dom.esm.js",
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^10.4.6",
|
||||
"@testing-library/react-hooks": "^3.3.0",
|
||||
"@types/jest": "^26.0.4",
|
||||
"@types/react": "^16.9.43",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"bundlesize": "^0.18.0",
|
||||
"codecov": "^3.7.0",
|
||||
"husky": "^4.2.5",
|
||||
"jest-marbles": "^2.5.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
"rxjs": "^6.6.0",
|
||||
"tsdx": "^0.13.2",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.6"
|
||||
}
|
||||
}
|
||||
53
packages/dom/src/batchUpdates.ts
Normal file
53
packages/dom/src/batchUpdates.ts
Normal file
@ -0,0 +1,53 @@
|
||||
import { Observable } from "rxjs"
|
||||
import { unstable_batchedUpdates } from "react-dom"
|
||||
|
||||
const noop = () => {}
|
||||
|
||||
/**
|
||||
* A RxJS pipeable operator which observes the source observable on
|
||||
* an asapScheduler and uses `ReactDom.unstable_batchedUpdates` to emmit the
|
||||
* values. It's useful for observing streams of events that come from outside
|
||||
* of ReactDom event-handlers.
|
||||
*
|
||||
* @remarks This operator will be deprecated when React 17 is released
|
||||
* (or whnever React CM is released). The reason being that React Concurrent Mode
|
||||
* automatically batches all synchrous updates. Meaning that with React CM,
|
||||
* observing a stream through the asapScheduler accomplishes the same thing.
|
||||
*/
|
||||
export const batchUpdates = <T>() => (
|
||||
source$: Observable<T>,
|
||||
): Observable<T> => {
|
||||
return new Observable<T>(observer => {
|
||||
let next: any = observer.next.bind(observer)
|
||||
let queue: T[] = []
|
||||
let promise: Promise<void> | null = null
|
||||
const flush = () => {
|
||||
promise = null
|
||||
const originalQueue = queue
|
||||
queue = []
|
||||
unstable_batchedUpdates(() => {
|
||||
originalQueue.forEach(x => next(x))
|
||||
})
|
||||
}
|
||||
const subscription = source$.subscribe({
|
||||
next(v) {
|
||||
queue.push(v)
|
||||
if (!promise) {
|
||||
promise = Promise.resolve().then(flush)
|
||||
}
|
||||
},
|
||||
complete() {
|
||||
next = noop
|
||||
observer.complete()
|
||||
},
|
||||
error(e) {
|
||||
next = noop
|
||||
observer.error(e)
|
||||
},
|
||||
})
|
||||
return () => {
|
||||
next = noop
|
||||
subscription.unsubscribe()
|
||||
}
|
||||
})
|
||||
}
|
||||
1
packages/dom/src/index.tsx
Normal file
1
packages/dom/src/index.tsx
Normal file
@ -0,0 +1 @@
|
||||
export { batchUpdates } from "./batchUpdates"
|
||||
33
packages/dom/tsconfig.json
Normal file
33
packages/dom/tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"include": ["src", "types", "test"],
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"lib": ["dom", "esnext"],
|
||||
"importHelpers": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"*": ["src/*", "node_modules/*"]
|
||||
},
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
7
packages/utils/jest.config.js
Normal file
7
packages/utils/jest.config.js
Normal file
@ -0,0 +1,7 @@
|
||||
module.exports = {
|
||||
globals: {
|
||||
"ts-jest": {
|
||||
diagnostics: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
65
packages/utils/package.json
Normal file
65
packages/utils/package.json
Normal file
@ -0,0 +1,65 @@
|
||||
{
|
||||
"version": "0.1.0-alpha.0",
|
||||
"bundlesize": [
|
||||
{
|
||||
"path": "./dist/react-rxjs/utils.cjs.production.min.js",
|
||||
"maxSize": "5 kB",
|
||||
"compression": "none"
|
||||
}
|
||||
],
|
||||
"sideEffects": false,
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/re-rxjs/react-rxjs.git"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "dist/index.js",
|
||||
"typings": "dist/index.d.ts",
|
||||
"files": [
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"start": "tsdx watch",
|
||||
"build": "tsdx build",
|
||||
"coverage": "codecov",
|
||||
"test": "tsdx test --passWithNoTests --coverage",
|
||||
"test:ci": "npm run test && npm run coverage && bundlesize",
|
||||
"lint": "tsdx lint",
|
||||
"prepare": "tsdx build"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"react": ">=16.8.0",
|
||||
"rxjs": ">=6"
|
||||
},
|
||||
"husky": {
|
||||
"hooks": {
|
||||
"pre-commit": "tsdx lint"
|
||||
}
|
||||
},
|
||||
"prettier": {
|
||||
"printWidth": 80,
|
||||
"semi": false,
|
||||
"trailingComma": "all"
|
||||
},
|
||||
"name": "@react-rxjs/utils",
|
||||
"author": "Josep M Sobrepere",
|
||||
"module": "dist/react-rxjs/utils.esm.js",
|
||||
"devDependencies": {
|
||||
"@testing-library/react": "^10.4.6",
|
||||
"@testing-library/react-hooks": "^3.3.0",
|
||||
"@types/jest": "^26.0.4",
|
||||
"@types/react": "^16.9.43",
|
||||
"@types/react-dom": "^16.9.8",
|
||||
"bundlesize": "^0.18.0",
|
||||
"codecov": "^3.7.0",
|
||||
"husky": "^4.2.5",
|
||||
"jest-marbles": "^2.5.1",
|
||||
"react": "^16.13.1",
|
||||
"react-dom": "^16.13.1",
|
||||
"react-test-renderer": "^16.13.1",
|
||||
"rxjs": "^6.6.0",
|
||||
"tsdx": "^0.13.2",
|
||||
"tslib": "^2.0.0",
|
||||
"typescript": "^3.9.6"
|
||||
}
|
||||
}
|
||||
35
packages/utils/src/continuousGroupBy.ts
Normal file
35
packages/utils/src/continuousGroupBy.ts
Normal file
@ -0,0 +1,35 @@
|
||||
import { Observable, Subject, GroupedObservable, BehaviorSubject } from 'rxjs';
|
||||
import { finalize, takeUntil, share } from 'rxjs/operators';
|
||||
|
||||
const continuousGroupBy = <I, O>(mapper: (x: I) => O) => (
|
||||
stream: Observable<I>
|
||||
) =>
|
||||
new Observable<GroupedObservable<O, I>>(subscriber => {
|
||||
const groups: Map<O, Subject<I>> = new Map();
|
||||
const sourceSubscriptionEnd: Subject<undefined> = new Subject();
|
||||
|
||||
return stream
|
||||
.subscribe(x => {
|
||||
const key = mapper(x);
|
||||
if (groups.has(key)) {
|
||||
return groups.get(key)!.next(x);
|
||||
}
|
||||
|
||||
const subject = new BehaviorSubject<I>(x);
|
||||
groups.set(key, subject);
|
||||
|
||||
const res = subject.pipe(
|
||||
finalize(() => groups.delete(key)),
|
||||
takeUntil(sourceSubscriptionEnd),
|
||||
share()
|
||||
) as GroupedObservable<O, I>;
|
||||
res.key = key;
|
||||
|
||||
subscriber.next(res);
|
||||
})
|
||||
.add(() => {
|
||||
sourceSubscriptionEnd.next();
|
||||
});
|
||||
});
|
||||
|
||||
export default continuousGroupBy;
|
||||
23
packages/utils/src/groupInMap.ts
Normal file
23
packages/utils/src/groupInMap.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { Observable, GroupedObservable, concat, of } from "rxjs"
|
||||
import { map, mergeMap, scan } from "rxjs/operators"
|
||||
import continuousGroupBy from "./continuousGroupBy"
|
||||
|
||||
const DELETE = Symbol("DELETE")
|
||||
export const groupInMap = <T, K, V>(
|
||||
keyGetter: (x: T) => K,
|
||||
projection: (x: GroupedObservable<K, T>) => Observable<V>,
|
||||
) => (source$: Observable<T>): Observable<Map<K, V>> =>
|
||||
source$.pipe(
|
||||
continuousGroupBy(keyGetter),
|
||||
mergeMap(inner$ =>
|
||||
concat(
|
||||
projection(inner$).pipe(map(v => [inner$.key, v] as const)),
|
||||
of([inner$.key, DELETE] as const),
|
||||
),
|
||||
),
|
||||
scan((acc, [key, value]) => {
|
||||
if (value !== DELETE) return acc.set(key, value)
|
||||
acc.delete(key)
|
||||
return acc
|
||||
}, new Map<K, V>()),
|
||||
)
|
||||
4
packages/utils/src/index.tsx
Normal file
4
packages/utils/src/index.tsx
Normal file
@ -0,0 +1,4 @@
|
||||
export { useSubscribe } from "./useSubscribe"
|
||||
export { Subscribe } from "./Subscribe"
|
||||
export { groupInMap } from "./groupInMap"
|
||||
export { mergeWithKey } from "./mergeWithKey"
|
||||
23
packages/utils/src/mergeWithKey.ts
Normal file
23
packages/utils/src/mergeWithKey.ts
Normal file
@ -0,0 +1,23 @@
|
||||
import { merge, Observable, ObservableInput, from, SchedulerLike } from "rxjs"
|
||||
import { map } from "rxjs/operators"
|
||||
|
||||
export const mergeWithKey: <
|
||||
O extends { [P in keyof any]: ObservableInput<any> },
|
||||
OT extends {
|
||||
[K in keyof O]: O[K] extends ObservableInput<infer V>
|
||||
? { type: K; payload: V }
|
||||
: unknown
|
||||
}
|
||||
>(
|
||||
x: O,
|
||||
concurrent?: number,
|
||||
scheduler?: SchedulerLike,
|
||||
) => Observable<OT[keyof O]> = (input, ...optionalArgs) =>
|
||||
merge(
|
||||
...(Object.entries(input)
|
||||
.map(
|
||||
([type, stream]) =>
|
||||
from(stream).pipe(map(payload => ({ type, payload } as any))) as any,
|
||||
)
|
||||
.concat(optionalArgs) as any[]),
|
||||
)
|
||||
33
packages/utils/tsconfig.json
Normal file
33
packages/utils/tsconfig.json
Normal file
@ -0,0 +1,33 @@
|
||||
{
|
||||
"include": ["src", "types", "test"],
|
||||
"compilerOptions": {
|
||||
"target": "es5",
|
||||
"module": "esnext",
|
||||
"lib": ["dom", "esnext"],
|
||||
"importHelpers": true,
|
||||
"declaration": true,
|
||||
"sourceMap": true,
|
||||
"rootDir": "./src",
|
||||
"strict": true,
|
||||
"noImplicitAny": true,
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true,
|
||||
"strictPropertyInitialization": true,
|
||||
"noImplicitThis": true,
|
||||
"alwaysStrict": true,
|
||||
"noUnusedLocals": true,
|
||||
"noUnusedParameters": true,
|
||||
"noImplicitReturns": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"moduleResolution": "node",
|
||||
"baseUrl": "./src",
|
||||
"paths": {
|
||||
"*": ["src/*", "node_modules/*"]
|
||||
},
|
||||
"jsx": "react",
|
||||
"esModuleInterop": true
|
||||
},
|
||||
"exclude": [
|
||||
"**/*.test.ts"
|
||||
]
|
||||
}
|
||||
@ -1,214 +0,0 @@
|
||||
import { scan } from "rxjs/operators"
|
||||
import { EMPTY_VALUE } from "./internal/empty-value"
|
||||
import { subjectFactory } from "./"
|
||||
|
||||
describe("createSubjectsFactory", () => {
|
||||
test("it releasess the subject when is no longer needed", () => {
|
||||
const getDiffs$ = subjectFactory<string, number>()
|
||||
const key = "foo"
|
||||
let fooSubject = getDiffs$(key)
|
||||
|
||||
expect(getDiffs$(key)).toBe(fooSubject)
|
||||
|
||||
const sub1 = getDiffs$(key).subscribe()
|
||||
const sub2 = getDiffs$(key).subscribe()
|
||||
const sub3 = getDiffs$(key).subscribe()
|
||||
|
||||
expect(getDiffs$(key)).toBe(fooSubject)
|
||||
expect(fooSubject.closed).toBe(false)
|
||||
|
||||
sub1.unsubscribe()
|
||||
sub2.unsubscribe()
|
||||
expect(getDiffs$(key)).toBe(fooSubject)
|
||||
expect(fooSubject.closed).toBe(false)
|
||||
|
||||
sub3.unsubscribe()
|
||||
expect(getDiffs$(key)).not.toBe(fooSubject)
|
||||
expect(fooSubject.closed).toBe(true)
|
||||
|
||||
fooSubject = getDiffs$(key)
|
||||
expect(getDiffs$(key)).toBe(fooSubject)
|
||||
|
||||
fooSubject.complete()
|
||||
expect(getDiffs$(key)).not.toBe(fooSubject)
|
||||
expect(fooSubject.closed).toBe(true)
|
||||
|
||||
fooSubject = getDiffs$(key)
|
||||
fooSubject.error("")
|
||||
expect(getDiffs$(key)).not.toBe(fooSubject)
|
||||
expect(fooSubject.closed).toBe(true)
|
||||
})
|
||||
|
||||
test("it multicasts", () => {
|
||||
const getDiffs$ = subjectFactory<string, number>()
|
||||
const key = "foo"
|
||||
|
||||
const initialState: {
|
||||
latest: number | undefined
|
||||
completed: boolean
|
||||
error: any
|
||||
} = {
|
||||
latest: undefined,
|
||||
completed: false,
|
||||
error: undefined,
|
||||
}
|
||||
|
||||
let sub1State = { ...initialState }
|
||||
getDiffs$(key).subscribe({
|
||||
next(v) {
|
||||
sub1State.latest = v
|
||||
},
|
||||
complete() {
|
||||
sub1State.completed = true
|
||||
},
|
||||
})
|
||||
|
||||
let sub2State = { ...initialState }
|
||||
getDiffs$(key).subscribe({
|
||||
next(v) {
|
||||
sub2State.latest = v
|
||||
},
|
||||
complete() {
|
||||
sub2State.completed = true
|
||||
},
|
||||
})
|
||||
|
||||
let sub3State = { ...initialState }
|
||||
getDiffs$(key).subscribe({
|
||||
next(v) {
|
||||
sub3State.latest = v
|
||||
},
|
||||
complete() {
|
||||
sub3State.completed = true
|
||||
},
|
||||
})
|
||||
|
||||
expect(sub1State.latest).toBe(undefined)
|
||||
expect(sub2State.latest).toBe(undefined)
|
||||
expect(sub3State.latest).toBe(undefined)
|
||||
|
||||
getDiffs$(key).next(10)
|
||||
expect(sub1State.latest).toBe(10)
|
||||
expect(sub2State.latest).toBe(10)
|
||||
expect(sub3State.latest).toBe(10)
|
||||
|
||||
getDiffs$(key).next(100)
|
||||
expect(sub1State.latest).toBe(100)
|
||||
expect(sub2State.latest).toBe(100)
|
||||
expect(sub3State.latest).toBe(100)
|
||||
|
||||
getDiffs$(key).complete()
|
||||
expect(sub1State.completed).toBe(true)
|
||||
expect(sub2State.completed).toBe(true)
|
||||
expect(sub3State.completed).toBe(true)
|
||||
|
||||
sub1State = { ...initialState }
|
||||
getDiffs$(key).subscribe({
|
||||
next(v) {
|
||||
sub1State.latest = v
|
||||
},
|
||||
complete() {
|
||||
sub1State.completed = true
|
||||
},
|
||||
error(e) {
|
||||
sub1State.error = e
|
||||
},
|
||||
})
|
||||
|
||||
sub2State = { ...initialState }
|
||||
getDiffs$(key).subscribe({
|
||||
next(v) {
|
||||
sub2State.latest = v
|
||||
},
|
||||
complete() {
|
||||
sub2State.completed = true
|
||||
},
|
||||
error(e) {
|
||||
sub2State.error = e
|
||||
},
|
||||
})
|
||||
|
||||
sub3State = { ...initialState }
|
||||
getDiffs$(key).subscribe({
|
||||
next(v) {
|
||||
sub3State.latest = v
|
||||
},
|
||||
complete() {
|
||||
sub3State.completed = true
|
||||
},
|
||||
error(e) {
|
||||
sub3State.error = e
|
||||
},
|
||||
})
|
||||
|
||||
expect(sub1State.latest).toBe(undefined)
|
||||
expect(sub2State.latest).toBe(undefined)
|
||||
expect(sub3State.latest).toBe(undefined)
|
||||
|
||||
getDiffs$(key).next(10)
|
||||
expect(sub1State.latest).toBe(10)
|
||||
expect(sub2State.latest).toBe(10)
|
||||
expect(sub3State.latest).toBe(10)
|
||||
|
||||
getDiffs$(key).error("error")
|
||||
expect(sub1State.error).toBe("error")
|
||||
expect(sub2State.error).toBe("error")
|
||||
expect(sub3State.error).toBe("error")
|
||||
|
||||
getDiffs$(key).next(1000)
|
||||
expect(sub1State.latest).toBe(10)
|
||||
expect(sub2State.latest).toBe(10)
|
||||
expect(sub3State.latest).toBe(10)
|
||||
})
|
||||
|
||||
test("the first subscription doesn't receive anything", () => {
|
||||
const getDiffs$ = subjectFactory<string, number>()
|
||||
const key = "foo"
|
||||
let value: number | typeof EMPTY_VALUE = EMPTY_VALUE
|
||||
getDiffs$(key).next(10)
|
||||
const sub = getDiffs$(key).subscribe(x => {
|
||||
value = x
|
||||
})
|
||||
expect(value).toBe(EMPTY_VALUE)
|
||||
sub.unsubscribe()
|
||||
})
|
||||
|
||||
test("it accepts void inputs", () => {
|
||||
const getClicks$ = subjectFactory<string, void>()
|
||||
const key = "foo"
|
||||
|
||||
let latestValue: number | undefined = undefined
|
||||
const sub = getClicks$(key)
|
||||
.pipe(scan(prev => prev + 1, 0))
|
||||
.subscribe(x => {
|
||||
latestValue = x
|
||||
})
|
||||
|
||||
expect(latestValue).toBe(undefined)
|
||||
|
||||
getClicks$(key).next()
|
||||
getClicks$(key).next()
|
||||
getClicks$(key).next()
|
||||
getClicks$(key).next()
|
||||
|
||||
expect(latestValue).toBe(4)
|
||||
|
||||
sub.unsubscribe()
|
||||
})
|
||||
|
||||
test("it does not replay values to new subscriptions", () => {
|
||||
const getDiffs$ = subjectFactory<string, number>()
|
||||
const key = "foo"
|
||||
const sub1 = getDiffs$(key).subscribe()
|
||||
getDiffs$(key).next(100)
|
||||
|
||||
let value = EMPTY_VALUE as any
|
||||
const sub2 = getDiffs$(key).subscribe(x => {
|
||||
value = x
|
||||
})
|
||||
|
||||
expect(value).toBe(EMPTY_VALUE)
|
||||
sub1.unsubscribe()
|
||||
sub2.unsubscribe()
|
||||
})
|
||||
})
|
||||
@ -1,41 +0,0 @@
|
||||
import { Subject, Observable, Observer } from "rxjs"
|
||||
import { finalize, share } from "rxjs/operators"
|
||||
|
||||
/**
|
||||
* Creates a pool of Subjects identified by key, and returns:
|
||||
* - A function that accepts a key and returns the Subject linked to that key.
|
||||
*
|
||||
* @remarks Strictly speaking the returned value is not a real Subject. It's in
|
||||
* fact a multicasted Observable that it's also an Observer. That's because in
|
||||
* order to prevent memory-leaks this cached Observable will be removed from the
|
||||
* cache when it finalizes.
|
||||
*/
|
||||
type ObserverObservable<T> = Observable<T> & Observer<T>
|
||||
export const subjectFactory = <K, V>() => {
|
||||
const cache = new Map<K, ObserverObservable<V>>()
|
||||
return (key: K) => {
|
||||
let result = cache.get(key)
|
||||
if (result) return result
|
||||
|
||||
const subject = new Subject<V>()
|
||||
const close = () => {
|
||||
result!.closed = true
|
||||
cache.delete(key)
|
||||
}
|
||||
result = subject.pipe(finalize(close), share()) as ObserverObservable<V>
|
||||
|
||||
result.closed = false
|
||||
result.next = subject.next.bind(subject)
|
||||
result.complete = () => {
|
||||
close()
|
||||
subject.complete()
|
||||
}
|
||||
result.error = (e: any) => {
|
||||
close()
|
||||
subject.error(e)
|
||||
}
|
||||
|
||||
cache.set(key, result)
|
||||
return result
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user