Skip to content

Commit 162abfd

Browse files
Brooooooklynforehalo
authored andcommitted
feat(ssr): support ssr
1 parent a885bea commit 162abfd

29 files changed

+858
-117
lines changed

.eslintrc.json

+2
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,12 @@
2222
}
2323
],
2424
"@typescript-eslint/explicit-function-return-type": "off",
25+
"@typescript-eslint/no-non-null-assertion": "off",
2526
"@typescript-eslint/no-parameter-properties": "off",
2627
"@typescript-eslint/no-use-before-define": ["error", { "functions": false, "classes": false }],
2728
"@typescript-eslint/no-explicit-any": "off",
2829
"@typescript-eslint/no-var-requires": "off",
30+
"@typescript-eslint/ban-ts-ignore": "off",
2931
"no-console": ["error", { "allow": ["warn", "error"] }]
3032
},
3133
"settings": {

jest.config.js

+4
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,8 @@
11
module.exports = {
22
preset: 'ts-jest',
33
testEnvironment: 'node',
4+
testMatch: [
5+
'**/__tests__/**/?(*.)+(spec|test).ts?(x)', '**/?(*.)+(spec|test).ts?(x)',
6+
],
7+
globalSetup: './setup.js'
48
}

package.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
"main": "./dist/index.js",
66
"module": "./esm/index.js",
77
"esnext": "./esnext/index.js",
8-
"types": "./dist/index.d.ts",
8+
"types": "./esm/index.d.ts",
99
"sideEffects": false,
1010
"repository": {
1111
"type": "git",
@@ -62,6 +62,7 @@
6262
},
6363
"devDependencies": {
6464
"@asuka/di": "^0.2.0",
65+
"@types/express": "^4.17.0",
6566
"@types/jest": "^24.0.16",
6667
"@types/lodash": "^4.14.136",
6768
"@types/node": "^12.6.9",

setup.js

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
module.exports = function setupTestEnv() {
2+
process.env.ENABLE_AYANAMI_SSR = 'false'
3+
}

src/core/ayanami.ts

+40-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,50 @@
1-
import { Observable } from 'rxjs'
1+
import { Observable, noop } from 'rxjs'
22

33
import { ActionOfAyanami } from './types'
44
import { combineWithIkari, destroyIkariFrom } from './ikari'
5+
import { moduleNameKey, globalKey } from '../ssr/ssr-module'
6+
import { isSSREnabled } from '../ssr/flag'
7+
8+
const globalScope =
9+
typeof self !== 'undefined'
10+
? self
11+
: typeof window !== 'undefined'
12+
? window
13+
: typeof global !== 'undefined'
14+
? global
15+
: {}
516

617
export abstract class Ayanami<State> {
718
abstract defaultState: State
819

20+
// @internal
21+
ssrLoadKey = Symbol('SSR_LOADED')
22+
23+
// @internal
24+
scopeName!: string
25+
26+
constructor() {
27+
if (!isSSREnabled()) {
28+
const name = Object.getPrototypeOf(this)[moduleNameKey]
29+
if (!name) {
30+
return
31+
}
32+
// @ts-ignore
33+
const globalCache = globalScope[globalKey]
34+
35+
if (globalCache) {
36+
const moduleCache = globalCache[name]
37+
if (moduleCache) {
38+
Reflect.defineMetadata(this.ssrLoadKey, true, this)
39+
Object.defineProperty(this, 'defaultState', {
40+
get: () => moduleCache[this.scopeName],
41+
set: noop,
42+
})
43+
}
44+
}
45+
}
46+
}
47+
948
destroy() {
1049
destroyIkariFrom(this)
1150
}

src/core/decorators/action-related.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,9 @@ import { Ayanami } from '../ayanami'
33
import { ConstructorOf } from '../types'
44

55
export function createActionDecorator(symbols: ActionSymbols) {
6-
return () => ({ constructor }: any, propertyKey: string) => {
7-
addActionName(symbols, constructor, propertyKey)
6+
return () => (target: any, propertyKey: string, descriptor: PropertyDescriptor) => {
7+
addActionName(symbols, target.constructor, propertyKey)
8+
return descriptor
89
}
910
}
1011

src/core/decorators/index.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import { createActionDecorator } from './action-related'
88
export * from './action-related'
99

1010
interface DecoratorReturnType<V> {
11-
(target: any, propertyKey: string, descriptor: { value?: V }): void
11+
(target: any, propertyKey: string, descriptor: { value?: V }): PropertyDescriptor
1212
}
1313

1414
export const ImmerReducer: <S = any>() => DecoratorReturnType<
@@ -23,4 +23,4 @@ export const Effect: <A = any, S = any>() => DecoratorReturnType<
2323
(action: Observable<A>, state$: Observable<S>) => Observable<EffectAction>
2424
> = createActionDecorator(effectSymbols)
2525

26-
export const DefineAction = createActionDecorator(defineActionSymbols)
26+
export const DefineAction: () => any = createActionDecorator(defineActionSymbols)

src/core/ikari.ts

+30-16
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { merge, Observable, Subject, Subscription, NEVER } from 'rxjs'
2-
import { map, catchError } from 'rxjs/operators'
2+
import { map, catchError, takeUntil, filter } from 'rxjs/operators'
33
import { mapValues } from 'lodash'
44
import produce from 'immer'
55

@@ -14,9 +14,11 @@ import {
1414
EffectActionFactories,
1515
} from './types'
1616
import { Ayanami } from './ayanami'
17-
import { BasicState, getEffectActionFactories, getOriginalFunctions } from './utils'
17+
import { createState, getEffectActionFactories, getOriginalFunctions } from './utils'
1818
import { logStateAction } from '../redux-devtools-extension'
1919
import { ikariSymbol } from './symbols'
20+
import { TERMINATE_ACTION } from '../ssr/terminate'
21+
import { isSSREnabled } from '../ssr/flag'
2022

2123
interface Config<State> {
2224
nameForLog: string
@@ -74,13 +76,13 @@ export function destroyIkariFrom<S>(ayanami: Ayanami<S>): void {
7476
}
7577

7678
export class Ikari<State> {
77-
static createAndBindAt<S>(target: { defaultState: S }, config: Config<S>): Ikari<S> {
79+
static createAndBindAt<S>(target: Ayanami<S>, config: Config<S>): Ikari<S> {
7880
const createdIkari = this.getFrom(target)
7981

8082
if (createdIkari) {
8183
return createdIkari
8284
} else {
83-
const ikari = new Ikari(config)
85+
const ikari = new Ikari(target, config)
8486
Reflect.defineMetadata(ikariSymbol, ikari, target)
8587
return ikari
8688
}
@@ -90,18 +92,18 @@ export class Ikari<State> {
9092
return Reflect.getMetadata(ikariSymbol, target)
9193
}
9294

93-
state: BasicState<State>
95+
state = createState(this.config.defaultState)
9496

95-
effectActionFactories: EffectActionFactories
97+
effectActionFactories = this.config.effectActionFactories
9698

9799
triggerActions: TriggerActions = {}
98100

99101
subscription = new Subscription()
100102

101-
constructor(private readonly config: Readonly<Config<State>>) {
102-
this.effectActionFactories = config.effectActionFactories
103-
this.state = new BasicState<State>(config.defaultState)
103+
// @internal
104+
terminate$ = new Subject<typeof TERMINATE_ACTION | null>()
104105

106+
constructor(readonly ayanami: Ayanami<State>, private readonly config: Readonly<Config<State>>) {
105107
const [effectActions$, effectActions] = setupEffectActions(
106108
this.config.effects,
107109
this.state.state$,
@@ -124,8 +126,18 @@ export class Ikari<State> {
124126
...mapValues(this.config.defineActions, ({ next }) => next),
125127
}
126128

129+
let effectActionsWithTerminate$: Observable<Action<any>>
130+
131+
if (!isSSREnabled()) {
132+
effectActionsWithTerminate$ = effectActions$
133+
} else {
134+
effectActionsWithTerminate$ = effectActions$.pipe(
135+
takeUntil(this.terminate$.pipe(filter((action) => action === null))),
136+
)
137+
}
138+
127139
this.subscription.add(
128-
effectActions$.subscribe((action) => {
140+
effectActionsWithTerminate$.subscribe((action) => {
129141
this.log(action)
130142
this.handleAction(action)
131143
}),
@@ -152,12 +164,10 @@ export class Ikari<State> {
152164
}
153165

154166
private log = ({ originalActionName, effectAction, reducerAction }: Action<State>) => {
155-
if (effectAction) {
167+
if (effectAction && effectAction !== TERMINATE_ACTION) {
156168
logStateAction(this.config.nameForLog, {
157169
params: effectAction.params,
158-
actionName: `${originalActionName}/👉${effectAction.ayanami.constructor.name}/️${
159-
effectAction.actionName
160-
}`,
170+
actionName: `${originalActionName}/👉${effectAction.ayanami.constructor.name}/️${effectAction.actionName}`,
161171
})
162172
}
163173

@@ -172,8 +182,12 @@ export class Ikari<State> {
172182

173183
private handleAction = ({ effectAction, reducerAction }: Action<State>) => {
174184
if (effectAction) {
175-
const { ayanami, actionName, params } = effectAction
176-
combineWithIkari(ayanami).triggerActions[actionName](params)
185+
if (effectAction !== TERMINATE_ACTION) {
186+
const { ayanami, actionName, params } = effectAction
187+
combineWithIkari(ayanami).triggerActions[actionName](params)
188+
} else {
189+
this.terminate$.next(effectAction)
190+
}
177191
}
178192

179193
if (reducerAction) {

src/core/scope/__test__/scope.spec.ts

+5-14
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,9 @@ import 'reflect-metadata'
22
import { Injectable } from '@asuka/di'
33

44
import { getInstanceWithScope, TransientScope, SameScope } from '../'
5-
import { createNewInstance, createOrGetInstanceInScope } from '../utils'
5+
import { createOrGetInstanceInScope } from '../utils'
66

77
describe('Scope spec:', () => {
8-
describe('createNewInstance', () => {
9-
it('should always return new instance', () => {
10-
class Test {}
11-
12-
expect(createNewInstance(Test)).toBeInstanceOf(Test)
13-
expect(createNewInstance(Test) === createNewInstance(Test)).toBeFalsy()
14-
})
15-
})
16-
178
describe('createOrGetInstanceInScope', () => {
189
class Test {}
1910
const scope = 'Scope'
@@ -102,7 +93,7 @@ describe('Scope spec:', () => {
10293
}
10394

10495
it('should return same instance if is same scope', () => {
105-
const scope = 'scope'
96+
const scope = Symbol('scope')
10697
const b = getInstanceWithScope(B, scope)
10798
const c = getInstanceWithScope(C, scope)
10899

@@ -111,9 +102,9 @@ describe('Scope spec:', () => {
111102
})
112103

113104
it('should return different instance if is different scope', () => {
114-
const b = getInstanceWithScope(B, 'b')
115-
const c1 = getInstanceWithScope(C, 'c1')
116-
const c2 = getInstanceWithScope(C, 'c2')
105+
const b = getInstanceWithScope(B, Symbol('b'))
106+
const c1 = getInstanceWithScope(C, Symbol('c1'))
107+
const c2 = getInstanceWithScope(C, Symbol('c2'))
117108

118109
expect(b.a).toBeInstanceOf(A)
119110
expect(c1.a).toBeInstanceOf(A)

src/core/scope/index.ts

+6-13
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
1-
import { InjectableFactory, ValueProvider } from '@asuka/di'
1+
import { InjectableFactory } from '@asuka/di'
22

33
import { ConstructorOf } from '../types'
44
import { ScopeConfig } from './type'
5-
import { createNewInstance, createOrGetInstanceInScope } from './utils'
6-
import { getSameScopeInjectionParams, SameScope } from './same-scope-decorator'
5+
import { createOrGetInstanceInScope, createScopeWithRequest } from './utils'
6+
import { SameScope } from './same-scope-decorator'
77

8-
export { ScopeConfig, SameScope }
8+
export { ScopeConfig, SameScope, createScopeWithRequest }
99

1010
export const TransientScope = Symbol('scope:transient')
1111

@@ -15,19 +15,12 @@ export function getInstanceWithScope<T>(
1515
constructor: ConstructorOf<T>,
1616
scope: ScopeConfig['scope'] = SingletonScope,
1717
): T {
18-
const providers = getSameScopeInjectionParams(constructor).map(
19-
(sameScopeInjectionParam): ValueProvider => ({
20-
provide: sameScopeInjectionParam,
21-
useValue: getInstanceWithScope(sameScopeInjectionParam, scope),
22-
}),
23-
)
24-
2518
switch (scope) {
2619
case SingletonScope:
2720
return InjectableFactory.getInstance(constructor)
2821
case TransientScope:
29-
return createNewInstance(constructor, providers)
22+
return InjectableFactory.initialize(constructor)
3023
default:
31-
return createOrGetInstanceInScope(constructor, scope, providers)
24+
return createOrGetInstanceInScope(constructor, scope)
3225
}
3326
}
+5-15
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,11 @@
1-
const SameScopeMetadataKey = Symbol('SameScopeInjectionParams')
1+
export const SameScopeMetadataKey = Symbol('SameScopeInjectionParams')
22

3-
export function getSameScopeInjectionParams(target: any): any[] {
3+
export const SameScope = () => (target: any, _propertyKey: string, parameterIndex: number) => {
4+
let sameScopeInjectionParams: boolean[] = []
45
if (Reflect.hasMetadata(SameScopeMetadataKey, target)) {
5-
return Reflect.getMetadata(SameScopeMetadataKey, target)
6+
sameScopeInjectionParams = Reflect.getMetadata(SameScopeMetadataKey, target)
67
} else {
7-
const sameScopeInjectionParams: any[] = []
88
Reflect.defineMetadata(SameScopeMetadataKey, sameScopeInjectionParams, target)
9-
return sameScopeInjectionParams
109
}
11-
}
12-
13-
function addSameScopeInjectionParam(target: any, param: object) {
14-
const sameScopeInjectionParams = getSameScopeInjectionParams(target)
15-
sameScopeInjectionParams.push(param)
16-
}
17-
18-
export const SameScope = () => (target: any, _propertyKey: string, parameterIndex: number) => {
19-
const param = Reflect.getMetadata('design:paramtypes', target)[parameterIndex]
20-
addSameScopeInjectionParam(target, param)
10+
sameScopeInjectionParams[parameterIndex] = true
2111
}

0 commit comments

Comments
 (0)