Clean and update worker
parent
85e1d194cd
commit
7b2c44575f
|
@ -0,0 +1,9 @@
|
|||
import { DottedName } from 'modele-social'
|
||||
import Engine from 'publicodes'
|
||||
|
||||
declare module '@publicodes/worker' {
|
||||
interface UserConfig {
|
||||
engine: Engine<DottedName>
|
||||
// additionalActions: ActionType<'test', number[], number>
|
||||
}
|
||||
}
|
|
@ -1,361 +0,0 @@
|
|||
import { DottedName } from 'modele-social'
|
||||
import {
|
||||
createContext,
|
||||
DependencyList,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useTransition,
|
||||
} from 'react'
|
||||
|
||||
import { ProviderProps } from '@/components/Provider'
|
||||
import { useSetupSafeSituation } from '@/components/utils/EngineContext'
|
||||
import { useLazyPromise, usePromise } from '@/hooks/usePromise'
|
||||
|
||||
import { Actions } from './socialWorkerEngine.worker'
|
||||
import {
|
||||
createWorkerEngineClient,
|
||||
WorkerEngineClient,
|
||||
} from './workerEngineClient'
|
||||
|
||||
export type WorkerEngine = NonNullable<ReturnType<typeof useCreateWorkerEngine>>
|
||||
|
||||
// @ts-expect-error
|
||||
const WorkerEngineContext = createContext<WorkerEngine>()
|
||||
|
||||
// export const useWorkerEngineContext = () => {
|
||||
// const context = useContext(WorkerEngineContext)
|
||||
// if (!context) {
|
||||
// throw new Error(
|
||||
// 'You are trying to use the worker engine outside of its provider'
|
||||
// )
|
||||
// }
|
||||
|
||||
// return context
|
||||
// }
|
||||
|
||||
export const useWorkerEngine = () => {
|
||||
const context = useContext(WorkerEngineContext)
|
||||
|
||||
if (!context && !import.meta.env.SSR) {
|
||||
throw new Error(
|
||||
'You are trying to use the worker engine outside of its provider'
|
||||
)
|
||||
}
|
||||
|
||||
// if (!context) {
|
||||
// throw new Error(
|
||||
// 'You are trying to use the worker engine before it is ready'
|
||||
// )
|
||||
// }
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
export const WorkerEngineProvider = ({
|
||||
children,
|
||||
basename,
|
||||
workerClient,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
basename: ProviderProps['basename']
|
||||
workerClient: WorkerEngineClient<Actions>
|
||||
}) => {
|
||||
const workerEngine = useCreateWorkerEngine(basename, workerClient)
|
||||
|
||||
useSetupSafeSituation(workerEngine)
|
||||
|
||||
if (workerEngine === undefined) {
|
||||
return children
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkerEngineContext.Provider value={workerEngine}>
|
||||
{children}
|
||||
</WorkerEngineContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
// export type WorkerEngine = WorkerEngineClient<Actions>
|
||||
// let workerClient: | null = null
|
||||
// setTimeout(() => {
|
||||
// const preparedWorker = new SocialeWorkerEngine()
|
||||
// const workerClient: WorkerEngineClient<Actions> =
|
||||
// createWorkerEngineClient<Actions>(
|
||||
// new SocialeWorkerEngine(),
|
||||
// () => {},
|
||||
// // (engineId) =>
|
||||
// // setSituationVersion((situationVersion) => {
|
||||
// // // console.log('??? setSituationVersion original')
|
||||
|
||||
// // // situationVersion[engineId] =
|
||||
// // // typeof situationVersion[engineId] !== 'number'
|
||||
// // // ? 0
|
||||
// // // : situationVersion[engineId]++
|
||||
|
||||
// // // return situationVersion
|
||||
// // return situationVersion + 1
|
||||
// // }),
|
||||
// { basename: 'mon-entreprise' }
|
||||
// )
|
||||
// workerClient.test.onSituationChange = function (engineId) {
|
||||
// console.log('original onSituationChange')
|
||||
// }
|
||||
// // }, 50)
|
||||
// console.time('loading')
|
||||
|
||||
/**
|
||||
* This hook is used to create a worker engine.
|
||||
* @param basename
|
||||
*/
|
||||
export const useCreateWorkerEngine = (
|
||||
basename: ProviderProps['basename'],
|
||||
workerClient: WorkerEngineClient<Actions>
|
||||
) => {
|
||||
const [situationVersion, setSituationVersion] = useState(0)
|
||||
const [workerEngine, setWorkerEngine] =
|
||||
useState<WorkerEngineClient<Actions>>(workerClient)
|
||||
// console.log('llllllpppppppppppppppppppppppppp', workerClient)
|
||||
|
||||
const [transition, startTransition] = useTransition()
|
||||
|
||||
useEffect(() => {
|
||||
console.timeEnd('time')
|
||||
// const workerClient = createWorkerEngineClient<Actions>(
|
||||
// new SocialeWorkerEngine(),
|
||||
// // () => {},
|
||||
// // () =>
|
||||
// // startTransition(() => {
|
||||
// // setSituationVersion((situationVersion) => {
|
||||
// // // console.log('??? setSituationVersion original')
|
||||
|
||||
// // // situationVersion[engineId] =
|
||||
// // // typeof situationVersion[engineId] !== 'number'
|
||||
// // // ? 0
|
||||
// // // : situationVersion[engineId]++
|
||||
|
||||
// // // return situationVersion
|
||||
// // return situationVersion + 1
|
||||
// // })
|
||||
// // }),
|
||||
// //
|
||||
// {
|
||||
// initParams: [{ basename: 'mon-entreprise' }],
|
||||
// onSituationChange: function () {
|
||||
// console.log('update *****************')
|
||||
|
||||
// startTransition(() => {
|
||||
// setSituationVersion((situationVersion) => {
|
||||
// return situationVersion + 1
|
||||
// })
|
||||
// })
|
||||
// },
|
||||
// }
|
||||
// )
|
||||
|
||||
workerClient.onSituationChange = function () {
|
||||
console.log('update *****************')
|
||||
|
||||
startTransition(() => {
|
||||
setSituationVersion((situationVersion) => {
|
||||
return situationVersion + 1
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// workerClient.context.onSituationChange = function () {
|
||||
// console.log('update !!!!!!!!!!!!!!!!!!')
|
||||
|
||||
// console.log('transition...')
|
||||
|
||||
// startTransition(() => {
|
||||
// setSituationVersion((situationVersion) => {
|
||||
// // console.log('??? setSituationVersion original')
|
||||
|
||||
// // situationVersion[engineId] =
|
||||
// // typeof situationVersion[engineId] !== 'number'
|
||||
// // ? 0
|
||||
// // : situationVersion[engineId]++
|
||||
|
||||
// // return situationVersion
|
||||
// return situationVersion + 1
|
||||
// })
|
||||
// })
|
||||
// }
|
||||
|
||||
console.log('{init worker}', workerClient)
|
||||
setWorkerEngine(workerClient)
|
||||
|
||||
// void workerClient.context.asyncSetSituation({})
|
||||
|
||||
console.time('{init}')
|
||||
let init = false
|
||||
void workerClient.isWorkerReady.finally(() => {
|
||||
init = true
|
||||
console.timeEnd('{init}')
|
||||
})
|
||||
|
||||
// example of usage
|
||||
// void Promise.all([
|
||||
// workerClient
|
||||
// .asyncEvaluate('SMIC')
|
||||
// .then((result) => console.log('{result}', result)),
|
||||
// workerClient
|
||||
// .asyncEvaluate('date')
|
||||
// .then((result) => console.log('{result}', result)),
|
||||
// ])
|
||||
|
||||
return () => {
|
||||
!init && console.timeEnd('{init}')
|
||||
console.log('{terminate worker}', workerClient)
|
||||
|
||||
// workerClient.terminate()
|
||||
}
|
||||
}, [basename])
|
||||
|
||||
// return workerEngine ? { ...workerEngine, situationVersion } : null
|
||||
const memo = useMemo(() => {
|
||||
// console.log('update:', { situationVersion, workerEngine })
|
||||
|
||||
return workerEngine ? { ...workerEngine, situationVersion } : undefined
|
||||
}, [situationVersion, workerEngine])
|
||||
|
||||
return memo
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
// const useSituationVersion = (workerEngineCtx: WorkerEngineCtx) =>
|
||||
// workerEngineCtx.situationVersion
|
||||
// [engineId]
|
||||
|
||||
interface Options<Default> {
|
||||
defaultValue?: Default
|
||||
workerEngine?: WorkerEngine
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper around usePromise that adds the situation version to the dependencies
|
||||
* @example const date = usePromiseOnSituationChange(() => asyncEvaluate('date'), []) // date will be updated when the situation changes
|
||||
* @deprecated
|
||||
*/
|
||||
export const usePromiseOnSituationChange = <T, Default = undefined>(
|
||||
promise: () => Promise<T>,
|
||||
deps: DependencyList,
|
||||
{ defaultValue, workerEngine: workerEngineOption }: Options<Default> = {}
|
||||
): T | Default => {
|
||||
const defaultWorkerEngineCtx = useWorkerEngine()
|
||||
const { situationVersion } = workerEngineOption ?? defaultWorkerEngineCtx
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const state = usePromise(promise, [...deps, situationVersion], defaultValue)
|
||||
|
||||
return state
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated
|
||||
*/
|
||||
export const useLazyPromiseOnSituationChange = <T, Default = undefined>(
|
||||
promise: () => Promise<T>,
|
||||
deps: DependencyList,
|
||||
{ defaultValue, workerEngine: workerEngineOption }: Options<Default> = {}
|
||||
): [T | Default, () => Promise<T>] => {
|
||||
const defaultWorkerEngineCtx = useWorkerEngine()
|
||||
const { situationVersion } = workerEngineOption ?? defaultWorkerEngineCtx
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
const tuple = useLazyPromise(
|
||||
promise,
|
||||
[...deps, situationVersion],
|
||||
defaultValue
|
||||
)
|
||||
|
||||
return tuple
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook is used to get a rule in the worker.
|
||||
* @param dottedName
|
||||
* @param options
|
||||
*/
|
||||
export const useAsyncGetRule = <
|
||||
// T extends unknown = undefined,
|
||||
Default = undefined,
|
||||
>(
|
||||
dottedName: DottedName,
|
||||
{ defaultValue, workerEngine: workerEngineOption }: Options<Default> = {}
|
||||
) => {
|
||||
const defaultWorkerEngine = useWorkerEngine()
|
||||
const workerEngine = workerEngineOption ?? defaultWorkerEngine
|
||||
|
||||
return usePromiseOnSituationChange(
|
||||
async () => workerEngine.asyncGetRule(dottedName),
|
||||
[dottedName, workerEngine],
|
||||
{ defaultValue, workerEngine }
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook is used to get parsed rules in the worker.
|
||||
* @param engineId
|
||||
*/
|
||||
export const useAsyncParsedRules = <
|
||||
Default = undefined, //
|
||||
>({
|
||||
defaultValue,
|
||||
workerEngine: workerEngineOption,
|
||||
}: Options<Default> = {}) => {
|
||||
const defaultWorkerEngine = useWorkerEngine()
|
||||
const workerEngine = workerEngineOption ?? defaultWorkerEngine
|
||||
|
||||
return usePromiseOnSituationChange(
|
||||
async () => workerEngine.asyncGetParsedRules(),
|
||||
[workerEngine],
|
||||
{ defaultValue, workerEngine }
|
||||
)
|
||||
}
|
||||
|
||||
export const useShallowCopy = (
|
||||
workerEngine: WorkerEngine
|
||||
): WorkerEngine | undefined => {
|
||||
const [situationVersion, setSituationVersion] = useState(0)
|
||||
|
||||
// const defaultWorkerEngine = useWorkerEngine()
|
||||
// const workerEngine = workerEngineParam
|
||||
// ?? defaultWorkerEngine
|
||||
|
||||
// console.log('??? situ version', situationVersion)
|
||||
|
||||
const workerEngineCopy = usePromiseOnSituationChange(
|
||||
async () => {
|
||||
const copy = await workerEngine.asyncShallowCopy(() => {
|
||||
// console.log('??? onSituationChange', copy)
|
||||
|
||||
setSituationVersion((x) => x + 1)
|
||||
})
|
||||
|
||||
// copy.onSituationChange = (x) => {
|
||||
// console.log('??? onSituationChange', copy)
|
||||
|
||||
// setSituationVersion(x)
|
||||
// }
|
||||
|
||||
// console.log('??? xxxxxxxxxxxxxxxxxxxxxxxxxxx', copy)
|
||||
|
||||
return copy
|
||||
},
|
||||
[workerEngine],
|
||||
{ defaultValue: undefined, workerEngine }
|
||||
)
|
||||
|
||||
const memo = useMemo(
|
||||
() =>
|
||||
workerEngineCopy ? { ...workerEngineCopy, situationVersion } : undefined,
|
||||
[situationVersion, workerEngineCopy]
|
||||
)
|
||||
|
||||
return memo
|
||||
}
|
|
@ -1,12 +1,17 @@
|
|||
import rawRules, { DottedName } from 'modele-social'
|
||||
import { createWorkerEngine, WorkerEngineActions } from '@publicodes/worker'
|
||||
import rawRules from 'modele-social'
|
||||
import Engine from 'publicodes'
|
||||
import {
|
||||
publicodesReactActions,
|
||||
PublicodesReactActions,
|
||||
} from 'publicodes-react'
|
||||
|
||||
import type { ProviderProps } from '@/components/Provider'
|
||||
import i18n from '@/locales/i18n'
|
||||
import ruleTranslations from '@/locales/rules-en.yaml'
|
||||
import translateRules from '@/locales/translateRules'
|
||||
|
||||
import { createWorkerEngine, WorkerEngineActions } from './workerEngine'
|
||||
export type Actions = WorkerEngineActions | PublicodesReactActions
|
||||
|
||||
function getUnitKey(unit: string): string {
|
||||
const units = i18n.getResourceBundle('fr', 'units') as Record<string, string>
|
||||
|
@ -52,11 +57,13 @@ const init = ({ basename }: Pick<ProviderProps, 'basename'>) => {
|
|||
|
||||
const engine = new Engine(rules, { getUnitKey, logger })
|
||||
|
||||
console.timeEnd('[createWorkerEngine] init')
|
||||
|
||||
return engine
|
||||
}
|
||||
|
||||
export type Actions = WorkerEngineActions<Parameters<typeof init>, DottedName>
|
||||
console.time('[createWorkerEngine] init')
|
||||
|
||||
console.time('[createWorkerEngine]')
|
||||
createWorkerEngine(init)
|
||||
console.timeEnd('[createWorkerEngine]')
|
||||
createWorkerEngine(init, {
|
||||
...publicodesReactActions(),
|
||||
})
|
||||
|
|
|
@ -1,237 +0,0 @@
|
|||
import Engine from 'publicodes'
|
||||
|
||||
/**
|
||||
* This file run any publicodes engine in a web worker.
|
||||
*/
|
||||
|
||||
export type WorkerEngineActions<
|
||||
InitParams extends unknown[] = unknown[],
|
||||
Name extends string = string,
|
||||
> =
|
||||
| {
|
||||
action: 'init'
|
||||
params: InitParams
|
||||
result: number
|
||||
}
|
||||
| {
|
||||
action: 'setSituation'
|
||||
params: Parameters<Engine<Name>['setSituation']>
|
||||
result: void
|
||||
}
|
||||
| {
|
||||
action: 'evaluate'
|
||||
params: Parameters<Engine<Name>['evaluate']>
|
||||
result: ReturnType<Engine<Name>['evaluate']>
|
||||
}
|
||||
| {
|
||||
action: 'getRule'
|
||||
params: Parameters<Engine<Name>['getRule']>
|
||||
result: ReturnType<Engine<Name>['getRule']>
|
||||
}
|
||||
| {
|
||||
action: 'getParsedRules'
|
||||
params: []
|
||||
result: ReturnType<Engine<Name>['getParsedRules']>
|
||||
}
|
||||
| {
|
||||
action: 'shallowCopy'
|
||||
params: [] // no params cause we use engineId
|
||||
result: number
|
||||
}
|
||||
| {
|
||||
action: 'deleteShallowCopy'
|
||||
params: [] // no params cause we use engineId
|
||||
result: void
|
||||
}
|
||||
|
||||
export type WorkerEngineAction<
|
||||
Actions extends WorkerEngineActions,
|
||||
Action extends Actions['action'],
|
||||
> = Extract<Actions, { action: Action }>
|
||||
|
||||
type GenericParams = {
|
||||
/**
|
||||
* The id of the engine to use, the default engine is 0
|
||||
*/
|
||||
engineId?: number
|
||||
|
||||
/**
|
||||
* The id of the message, used to identify the response
|
||||
*/
|
||||
id: number
|
||||
}
|
||||
|
||||
type DistributiveOmit<T, K extends keyof T> = T extends unknown
|
||||
? Omit<T, K>
|
||||
: never
|
||||
|
||||
/**
|
||||
*/
|
||||
export const createWorkerEngine = <
|
||||
Name extends string = string,
|
||||
InitParams extends unknown[] = unknown[],
|
||||
>(
|
||||
init: (...params: InitParams) => Engine<Name>
|
||||
) => {
|
||||
type Params = DistributiveOmit<
|
||||
WorkerEngineActions<InitParams, Name> & GenericParams,
|
||||
'result'
|
||||
>
|
||||
|
||||
let engines: (Engine<Name> | undefined)[] = []
|
||||
let queue: (Params & { engineId: number })[] = []
|
||||
|
||||
let setDefaultEngineReady: (() => void) | null = null
|
||||
const isDefaultEngineReady = new Promise(
|
||||
(resolve) => (setDefaultEngineReady = resolve as () => void)
|
||||
)
|
||||
|
||||
const actions = (
|
||||
data: Params
|
||||
// & { engines: EngineType[] }
|
||||
) => {
|
||||
const { engineId = 0, id, action, params } = data
|
||||
|
||||
const engine = engines[engineId]
|
||||
if (!engine) {
|
||||
throw new Error('Engine does not exist')
|
||||
}
|
||||
|
||||
if (action === 'setSituation') {
|
||||
// safeSetSituation(
|
||||
// engine,
|
||||
// ({ situation, <faultyDottedName> }) => {
|
||||
// console.error('setSituation', { situation, faultyDottedName })
|
||||
// },
|
||||
// ...params
|
||||
// )
|
||||
engine.setSituation(...params)
|
||||
|
||||
return { id }
|
||||
} else if (action === 'evaluate') {
|
||||
const result = engine.evaluate(...params)
|
||||
console.log('[result]', result)
|
||||
|
||||
return { id, result }
|
||||
} else if (action === 'getRule') {
|
||||
const result = engine.getRule(...params)
|
||||
|
||||
return { id, result }
|
||||
} else if (action === 'getParsedRules') {
|
||||
const result = engine.getParsedRules()
|
||||
|
||||
return { id, result }
|
||||
} else if (action === 'shallowCopy') {
|
||||
engines.push(engine.shallowCopy())
|
||||
|
||||
return { id, result: engines.length - 1 }
|
||||
} else if (action === 'deleteShallowCopy') {
|
||||
if (engineId === 0) {
|
||||
throw new Error('Cannot delete the default engine')
|
||||
}
|
||||
|
||||
delete engines[engineId]
|
||||
engines = engines.splice(engineId, 1)
|
||||
|
||||
console.log('[engines]', engines)
|
||||
|
||||
return { id }
|
||||
} else {
|
||||
console.log('[unknow message]', data)
|
||||
|
||||
return { id }
|
||||
}
|
||||
}
|
||||
|
||||
let timeout: NodeJS.Timeout | null = null
|
||||
onmessage = async (e) => {
|
||||
console.log('[onmessage]', e.data)
|
||||
|
||||
const { engineId = 0, id, action, params } = e.data as Params
|
||||
|
||||
try {
|
||||
if (action === 'init') {
|
||||
// console.log('[init engine]')
|
||||
// const [{ basename }] = params
|
||||
try {
|
||||
// let rules = rawRules
|
||||
// if (basename === 'infrance') {
|
||||
// // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
||||
// rules = translateRules('en', ruleTranslations, rules)
|
||||
// }
|
||||
// engineFactory(rules)
|
||||
|
||||
const engine = init(...params)
|
||||
|
||||
const engineId = engines.length
|
||||
engines.push(engine)
|
||||
|
||||
postMessage({ id, result: engineId })
|
||||
setDefaultEngineReady?.()
|
||||
console.log('[engine ready]', engines[engineId])
|
||||
} catch (e) {
|
||||
console.error('[error]', e)
|
||||
// postMessage('error')
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
await isDefaultEngineReady
|
||||
|
||||
queue.push({ engineId, id, action, params } as Params & {
|
||||
engineId: number
|
||||
})
|
||||
|
||||
if (timeout !== null) {
|
||||
return
|
||||
}
|
||||
|
||||
// timeout !== null && clearTimeout(timeout)
|
||||
timeout = setTimeout(() => {
|
||||
const aborts: number[] = []
|
||||
const setSituationEncountered: boolean[] = []
|
||||
const filteredQueue = [...queue]
|
||||
.reverse()
|
||||
.filter(({ action, engineId, id }) => {
|
||||
if (action === 'setSituation')
|
||||
setSituationEncountered[engineId] = true
|
||||
|
||||
const keep =
|
||||
!setSituationEncountered[engineId] ||
|
||||
(setSituationEncountered[engineId] && action !== 'evaluate')
|
||||
|
||||
if (!keep) aborts.push(id)
|
||||
|
||||
return keep
|
||||
})
|
||||
.reverse()
|
||||
console.log('[start queue]', queue, filteredQueue)
|
||||
|
||||
console.time('bench')
|
||||
postMessage({
|
||||
batch: filteredQueue.map((params) => {
|
||||
try {
|
||||
const res = actions(params)
|
||||
|
||||
return res
|
||||
} catch (error) {
|
||||
return { id: params.id, error }
|
||||
}
|
||||
}),
|
||||
})
|
||||
|
||||
console.timeEnd('bench')
|
||||
const error = new Error(
|
||||
'aborts the action because the situation has changed'
|
||||
)
|
||||
postMessage({ batch: aborts.map((id) => ({ id, error })) })
|
||||
|
||||
queue = []
|
||||
timeout = null
|
||||
}, 50)
|
||||
} catch (error) {
|
||||
return postMessage({ id, error })
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,286 +0,0 @@
|
|||
import type { WorkerEngineAction, WorkerEngineActions } from './workerEngine'
|
||||
|
||||
// if (typeof Worker === 'undefined') {
|
||||
// throw new Error('Worker is not supported.')
|
||||
// }
|
||||
|
||||
/**
|
||||
* This file is a client to communicate with workerEngine.
|
||||
*/
|
||||
|
||||
const isObject = (val: unknown): val is object =>
|
||||
typeof val === 'object' && val !== null
|
||||
|
||||
const isId = (val: object): val is { id: number } =>
|
||||
'id' in val && typeof val.id === 'number'
|
||||
|
||||
const isBatch = (val: object): val is { batch: unknown[] } =>
|
||||
'batch' in val && Array.isArray(val.batch)
|
||||
|
||||
interface WorkerEnginePromise<
|
||||
Actions extends WorkerEngineActions = WorkerEngineActions,
|
||||
ActionNames extends Actions['action'] = Actions['action'],
|
||||
// InitParams extends unknown[] = unknown[],
|
||||
// Name extends string = string,
|
||||
// T extends Actions['action'] = Actions['action'],
|
||||
> {
|
||||
engineId: number
|
||||
action: ActionNames
|
||||
resolve: (value: unknown) => void
|
||||
reject: (value: unknown) => void
|
||||
}
|
||||
|
||||
interface Ctx<
|
||||
Actions extends WorkerEngineActions = WorkerEngineActions,
|
||||
// Promises extends WorkerEnginePromise = WorkerEnginePromise,
|
||||
// >
|
||||
// Actions extends WorkerEngineActions<InitParams, Name>,
|
||||
// InitParams extends unknown[] = unknown[],
|
||||
// Name extends string = string,
|
||||
> {
|
||||
engineId: number
|
||||
promises: WorkerEnginePromise<Actions>[]
|
||||
lastCleanup: null | NodeJS.Timeout
|
||||
worker: Worker
|
||||
isWorkerReady: Promise<number>
|
||||
}
|
||||
|
||||
export type WorkerEngineClient<Actions extends WorkerEngineActions> =
|
||||
ReturnType<typeof createWorkerEngineClient<Actions>>
|
||||
|
||||
export const createWorkerEngineClient = <Actions extends WorkerEngineActions>(
|
||||
worker: Worker,
|
||||
options: {
|
||||
initParams: WorkerEngineAction<Actions, 'init'>['params']
|
||||
onSituationChange?: (engineId: number) => void
|
||||
}
|
||||
) => {
|
||||
console.log('{createWorker}')
|
||||
|
||||
const ctx: Ctx<Actions> = {
|
||||
engineId: 0,
|
||||
promises: [],
|
||||
lastCleanup: null,
|
||||
worker,
|
||||
isWorkerReady: null as unknown as Promise<number>, // will be set later in the function
|
||||
}
|
||||
|
||||
worker.onmessageerror = function (e) {
|
||||
console.log('{onmessageerror}', e)
|
||||
}
|
||||
|
||||
worker.onerror = function (e) {
|
||||
console.log('{onerror}', e)
|
||||
}
|
||||
|
||||
const ppp = (data: { id: number; result?: unknown; error?: string }) => {
|
||||
console.timeEnd(`execute-${data.id}`)
|
||||
if (data.id === 0) {
|
||||
console.timeEnd('loading')
|
||||
}
|
||||
|
||||
if ('error' in data) {
|
||||
return ctx.promises[data.id].reject?.(data.error)
|
||||
}
|
||||
ctx.promises[data.id].resolve?.(data.result)
|
||||
}
|
||||
|
||||
worker.onmessage = function (e: MessageEvent<unknown>) {
|
||||
const data = e.data
|
||||
|
||||
console.log('{msg}', data)
|
||||
|
||||
if (isObject(data)) {
|
||||
if (isId(data)) {
|
||||
ppp(data)
|
||||
|
||||
return
|
||||
} else if (isBatch(data)) {
|
||||
data.batch.forEach((d) => isObject(d) && isId(d) && ppp(d))
|
||||
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
console.error('{unknown message}', data)
|
||||
|
||||
throw new Error('unknown message')
|
||||
}
|
||||
|
||||
const { initParams, onSituationChange } = options
|
||||
|
||||
ctx.isWorkerReady = postMessage(ctx, 'init', ...initParams)
|
||||
|
||||
const workerEngine = workerEngineConstruct(ctx, { onSituationChange })
|
||||
|
||||
return workerEngine
|
||||
}
|
||||
|
||||
/**
|
||||
* Post message to worker engine and return a promise to get the result,
|
||||
* if the promise is not resolved in 10 seconds, it will be rejected.
|
||||
* @param ctx
|
||||
* @param action
|
||||
* @param params
|
||||
*/
|
||||
const postMessage = async <
|
||||
Actions extends WorkerEngineActions,
|
||||
ActionNames extends Actions['action'],
|
||||
Action extends WorkerEngineAction<Actions, ActionNames>,
|
||||
>(
|
||||
ctx: Ctx,
|
||||
action: ActionNames,
|
||||
...params: Action['params']
|
||||
) => {
|
||||
const { engineId, worker } = ctx
|
||||
|
||||
console.log('{postMessage}', action, params)
|
||||
|
||||
const promiseTimeout = 10000
|
||||
const warning = setTimeout(() => {
|
||||
console.log('{promise waiting for too long, aborting!}', action, params)
|
||||
ctx.promises[id].reject?.(new Error('timeout'))
|
||||
}, promiseTimeout)
|
||||
|
||||
ctx.lastCleanup !== null && clearInterval(ctx.lastCleanup)
|
||||
ctx.lastCleanup = setTimeout(() => {
|
||||
if (ctx.promises.length) {
|
||||
console.log('{cleanup}', ctx.promises.length)
|
||||
ctx.promises = []
|
||||
ctx.lastCleanup = null
|
||||
}
|
||||
}, promiseTimeout * 2)
|
||||
|
||||
const id = ctx.promises.length
|
||||
console.time(`execute-${id}`)
|
||||
|
||||
const stack = new Error().stack
|
||||
|
||||
const promise = new Promise<Action['result']>((resolve, reject) => {
|
||||
ctx.promises[id] = {
|
||||
engineId,
|
||||
action,
|
||||
resolve: (...params: unknown[]) => {
|
||||
clearTimeout(warning)
|
||||
|
||||
return resolve(...(params as Parameters<typeof resolve>))
|
||||
},
|
||||
reject: (err: unknown) => {
|
||||
clearTimeout(warning)
|
||||
|
||||
console.error(err)
|
||||
console.error(stack)
|
||||
console.error(new Error((err as Error).message, { cause: stack }))
|
||||
|
||||
return reject(err)
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
worker.postMessage({ engineId: ctx.engineId, action, params, id })
|
||||
|
||||
return promise
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
const wrappedPostMessage =
|
||||
(ctx: Ctx) =>
|
||||
<
|
||||
Actions extends WorkerEngineActions,
|
||||
ActionNames extends Actions['action'],
|
||||
Action extends WorkerEngineAction<Actions, ActionNames>,
|
||||
>(
|
||||
action: ActionNames,
|
||||
...params: Action['params']
|
||||
) =>
|
||||
postMessage<Actions, ActionNames, Action>(ctx, action, ...params)
|
||||
|
||||
/**
|
||||
*/
|
||||
const workerEngineConstruct = <Actions extends WorkerEngineActions>(
|
||||
ctx: Ctx<Actions>,
|
||||
options: {
|
||||
onSituationChange?: (engineId: number) => void
|
||||
}
|
||||
) => {
|
||||
type Action<T extends Actions['action']> = WorkerEngineAction<Actions, T>
|
||||
|
||||
const context = {
|
||||
engineId: ctx.engineId,
|
||||
worker: ctx.worker,
|
||||
isWorkerReady: ctx.isWorkerReady,
|
||||
onSituationChange: options.onSituationChange,
|
||||
postMessage: wrappedPostMessage(ctx),
|
||||
|
||||
terminate: () => {
|
||||
context.worker.terminate()
|
||||
ctx.promises.forEach((promise) => promise.reject?.('worker terminated'))
|
||||
ctx.promises = []
|
||||
},
|
||||
|
||||
/**
|
||||
* This function is used to set the situation in the worker with a specific engineId.
|
||||
*/
|
||||
asyncSetSituation: async (
|
||||
...params: Action<'setSituation'>['params']
|
||||
): Promise<Action<'setSituation'>['result']> => {
|
||||
const ret = await context.postMessage('setSituation', ...params)
|
||||
|
||||
context.onSituationChange?.(ctx.engineId)
|
||||
|
||||
return ret
|
||||
},
|
||||
|
||||
/**
|
||||
* This function is used to evaluate a publicodes expression in the worker with a specific engineId.
|
||||
*/
|
||||
asyncEvaluate: async (
|
||||
...params: Action<'evaluate'>['params']
|
||||
): Promise<Action<'evaluate'>['result']> => {
|
||||
const promise = await context.postMessage('evaluate', ...params)
|
||||
|
||||
// console.trace('{asyncEvaluate}')
|
||||
|
||||
return promise
|
||||
},
|
||||
|
||||
/**
|
||||
* This function is used to get a publicodes rule that is in the worker with a specific EngineId.
|
||||
*/
|
||||
asyncGetRule: async (
|
||||
...params: Action<'getRule'>['params']
|
||||
): Promise<Action<'getRule'>['result']> => {
|
||||
return await context.postMessage('getRule', ...params)
|
||||
},
|
||||
|
||||
/**
|
||||
* This function is used to get all the parsed rules in the worker with a specific engineId.
|
||||
*/
|
||||
asyncGetParsedRules: async (): Promise<
|
||||
Action<'getParsedRules'>['result']
|
||||
> => {
|
||||
return await context.postMessage('getParsedRules')
|
||||
},
|
||||
|
||||
/**
|
||||
* This function is used to shallow copy an engine in the worker with a specific engineId.
|
||||
*/
|
||||
asyncShallowCopy: async (onSituationChange: () => void = () => {}) => {
|
||||
const engineId = await context.postMessage('shallowCopy')
|
||||
|
||||
return workerEngineConstruct({ ...ctx, engineId }, { onSituationChange })
|
||||
},
|
||||
|
||||
/**
|
||||
* This function is used to delete a shallow copy of an engine in the worker.
|
||||
*/
|
||||
asyncDeleteShallowCopy: async (): Promise<
|
||||
Action<'deleteShallowCopy'>['result']
|
||||
> => {
|
||||
return context.postMessage('deleteShallowCopy')
|
||||
},
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
import { DottedName } from 'modele-social'
|
||||
import {
|
||||
createContext,
|
||||
useContext,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useState,
|
||||
useTransition,
|
||||
} from 'react'
|
||||
|
||||
import { useSetupSafeSituation } from '@/components/utils/EngineContext'
|
||||
import { usePromise } from '@/hooks/usePromise'
|
||||
|
||||
import { Actions } from './socialWorkerEngine.worker'
|
||||
import { WorkerEngineClient } from './workerEngineClient'
|
||||
|
||||
/**
|
||||
*/
|
||||
export const useSynchronizedWorkerEngine = (
|
||||
workerClient: WorkerEngineClient<Actions>
|
||||
) => {
|
||||
const [transition, startTransition] = useTransition()
|
||||
|
||||
const [situationVersion, setSituationVersion] = useState(0)
|
||||
const [workerEngine, setWorkerEngine] = useState<WorkerEngineClient<Actions>>(
|
||||
() => {
|
||||
workerClient.onSituationChange = function () {
|
||||
console.log('onSituationChange', workerClient.engineId)
|
||||
|
||||
startTransition(() => {
|
||||
setSituationVersion((situationVersion) => {
|
||||
return situationVersion + 1
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return workerClient
|
||||
}
|
||||
)
|
||||
|
||||
const memo = useMemo(() => {
|
||||
return { ...workerEngine, situationVersion }
|
||||
}, [situationVersion, workerEngine])
|
||||
|
||||
return memo
|
||||
}
|
||||
|
||||
export type WorkerEngine = NonNullable<
|
||||
ReturnType<typeof useSynchronizedWorkerEngine>
|
||||
>
|
||||
|
||||
const WorkerEngineContext = createContext<WorkerEngine>(
|
||||
undefined as unknown as WorkerEngine
|
||||
)
|
||||
|
||||
/**
|
||||
*/
|
||||
export const useWorkerEngine = () => {
|
||||
const context = useContext(WorkerEngineContext)
|
||||
|
||||
if (!context && !import.meta.env.SSR) {
|
||||
throw new Error(
|
||||
'You are trying to use the worker engine outside of its provider'
|
||||
)
|
||||
}
|
||||
|
||||
return context
|
||||
}
|
||||
|
||||
/**
|
||||
*/
|
||||
export const WorkerEngineProvider = ({
|
||||
workerClient,
|
||||
children,
|
||||
}: {
|
||||
workerClient: WorkerEngineClient<Actions>
|
||||
children: React.ReactNode
|
||||
}) => {
|
||||
const workerEngine = useSynchronizedWorkerEngine(workerClient)
|
||||
|
||||
useSetupSafeSituation(workerEngine)
|
||||
|
||||
if (workerEngine === undefined) {
|
||||
return children
|
||||
}
|
||||
|
||||
return (
|
||||
<WorkerEngineContext.Provider value={workerEngine}>
|
||||
{children}
|
||||
</WorkerEngineContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
interface Options<DefaultValue> {
|
||||
workerEngine?: WorkerEngine
|
||||
defaultValue?: DefaultValue
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook is used to get a rule in the worker engine.
|
||||
*/
|
||||
export const useAsyncGetRule = <
|
||||
DefaultValue = undefined, //
|
||||
>(
|
||||
dottedName: DottedName,
|
||||
{ defaultValue, workerEngine: workerEngineOption }: Options<DefaultValue> = {}
|
||||
) => {
|
||||
const defaultWorkerEngine = useWorkerEngine()
|
||||
const workerEngine = workerEngineOption ?? defaultWorkerEngine
|
||||
|
||||
return usePromise(
|
||||
async () => workerEngine.asyncGetRule(dottedName),
|
||||
[dottedName, workerEngine],
|
||||
defaultValue
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook is used to get parsed rules in the worker engine.
|
||||
*/
|
||||
export const useAsyncParsedRules = <
|
||||
DefaultValue = undefined, //
|
||||
>({
|
||||
workerEngine: workerEngineOption,
|
||||
defaultValue,
|
||||
}: Options<DefaultValue> = {}) => {
|
||||
const defaultWorkerEngine = useWorkerEngine()
|
||||
const workerEngine = workerEngineOption ?? defaultWorkerEngine
|
||||
|
||||
return usePromise(
|
||||
async () => workerEngine.asyncGetParsedRules(),
|
||||
[workerEngine],
|
||||
defaultValue
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* This hook is used to make a shallow copy of the worker engine.
|
||||
*/
|
||||
export const useShallowCopy = (
|
||||
workerEngine: WorkerEngine
|
||||
): WorkerEngine | undefined => {
|
||||
const [transition, startTransition] = useTransition()
|
||||
|
||||
const [situationVersion, setSituationVersion] = useState(0)
|
||||
|
||||
const workerEngineShallowCopy = usePromise(async () => {
|
||||
const copy = await workerEngine.asyncShallowCopy(() => {
|
||||
console.log('onSituationChange in shallow copy', copy.engineId)
|
||||
|
||||
startTransition(() => {
|
||||
setSituationVersion((x) => x + 1)
|
||||
})
|
||||
})
|
||||
|
||||
return copy
|
||||
}, [workerEngine])
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
console.log('deleteShallowCopy', workerEngineShallowCopy?.engineId)
|
||||
|
||||
void workerEngineShallowCopy?.asyncDeleteShallowCopy()
|
||||
},
|
||||
[workerEngineShallowCopy]
|
||||
)
|
||||
|
||||
const memo = useMemo(
|
||||
() =>
|
||||
workerEngineShallowCopy
|
||||
? { ...workerEngineShallowCopy, situationVersion }
|
||||
: undefined,
|
||||
[situationVersion, workerEngineShallowCopy]
|
||||
)
|
||||
|
||||
return memo
|
||||
}
|
||||
|
||||
export function useInversionFail() {
|
||||
// return useContext(EngineContext).inversionFail()
|
||||
}
|
Loading…
Reference in New Issue