monorepo: @react-rxjs/core @react-rxjs/utils @react-rxjs/dom

This commit is contained in:
Josep M Sobrepere 2020-07-16 01:32:53 +02:00
parent 1414c417ff
commit 431d56e16e
48 changed files with 7216 additions and 10903 deletions

View File

@ -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

File diff suppressed because it is too large Load Diff

View File

@ -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"
}
}

View 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"
}
}

View File

@ -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"

View File

@ -0,0 +1,7 @@
module.exports = {
globals: {
"ts-jest": {
diagnostics: false,
},
},
}

66
packages/dom/package.json Normal file
View 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"
}
}

View 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()
}
})
}

View File

@ -0,0 +1 @@
export { batchUpdates } from "./batchUpdates"

View 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"
]
}

View File

@ -0,0 +1,7 @@
module.exports = {
globals: {
"ts-jest": {
diagnostics: false,
},
},
}

View 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"
}
}

View 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;

View 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>()),
)

View File

@ -0,0 +1,4 @@
export { useSubscribe } from "./useSubscribe"
export { Subscribe } from "./Subscribe"
export { groupInMap } from "./groupInMap"
export { mergeWithKey } from "./mergeWithKey"

View 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[]),
)

View 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"
]
}

View File

@ -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()
})
})

View File

@ -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
}
}

6788
yarn.lock Normal file

File diff suppressed because it is too large Load Diff