Created
November 17, 2022 04:59
-
-
Save dnsosebee/52e03181bf8c9629a27bc627e11f6412 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* global postMessage */ | |
import { logger as parentLogger } from '../../logger' | |
import { MessageTypes } from './flogramming' | |
const logger = parentLogger.child({ module: 'worker' }) | |
export type GenericMessageToWorker<T extends MessageTypes> = { | |
type: T | |
toEval: string // can have escaped pointy brackets, or not | |
context: { [key: string]: unknown } | |
declaredIdentifiers: T extends MessageTypes.assign ? string[] : undefined | |
} | |
export type MessageToWorker = | |
| GenericMessageToWorker<MessageTypes.assign> | |
| GenericMessageToWorker<MessageTypes.expression> | |
export type GenericMessageFromWorker<T extends MessageTypes> = { | |
type: T | |
} & (T extends 'assign' ? { result: { [key: string]: unknown } } : { result: unknown }) | |
export type MessageFromWorker = | |
| { result: unknown | { [key: string]: unknown } } | |
| { error: unknown } | |
const assignContextCode = ( | |
context: { [key: string]: unknown }, | |
contextArgName = 'context', | |
): string => { | |
logger.debug('assignContextCode', { context }) | |
return ( | |
Object.keys(context) | |
.map(k => `let ${k} = ${contextArgName}.${k}`) | |
.join('; ') + ';' | |
) | |
} | |
const retrieveContextCode = ( | |
context: { [key: string]: unknown }, | |
declaredIdentifiers: string[], | |
): string => { | |
const keys = Object.keys(context) | |
const dedupedIdentifiers = [...new Set([...keys, ...declaredIdentifiers])] | |
return `return {${dedupedIdentifiers.map(k => `${k}`).join(', ')}}` | |
} | |
const evalExpression = (toEval: string, context: { [key: string]: unknown }): unknown => { | |
logger.debug('evalExpression', { toEval, context }) | |
const code = `${assignContextCode(context)} return ${toEval}` | |
logger.debug('evalExpression code', code) | |
return Function('context', code)(context) | |
} | |
const evalAssign = ( | |
toEval: string, | |
context: { [key: string]: unknown }, | |
declaredIdentifiers: string[], | |
): { [key: string]: unknown } => { | |
logger.debug('evalAssign', { toEval, context }) | |
const code = ` ${assignContextCode(context)} ${toEval}; ${retrieveContextCode( | |
context, | |
declaredIdentifiers, | |
)}` | |
logger.debug('evalAssign code', code) | |
return Function('context', code)(context) | |
} | |
onmessage = (e: MessageEvent<MessageToWorker>) => { | |
const { toEval, context, type } = e.data | |
logger.debug('onmessage', { toEval, context, type }) | |
const toEvalUnescaped = toEval.replaceAll(/</g, '<').replaceAll(/>/g, '>') | |
let result: MessageFromWorker | |
try { | |
if (type === 'expression') { | |
result = { result: evalExpression(toEvalUnescaped, context) } | |
} else if (type === 'assign') { | |
result = { result: evalAssign(toEvalUnescaped, context, e.data.declaredIdentifiers) } | |
} else { | |
throw new Error(`Unknown type: ${type}`) | |
} | |
} catch (e: unknown) { | |
result = { error: e } | |
} | |
postMessage(result) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { Map } from 'immutable' | |
import { logger as parentLogger } from '../../logger' | |
import { GenericMessageToWorker, MessageFromWorker, MessageToWorker } from './flogram.worker' | |
import { getDeclaredIdentifiers } from './parse' | |
const logger = parentLogger.child({ module: 'flogramming' }) | |
export enum MessageTypes { | |
expression = 'expression', | |
assign = 'assign', | |
} | |
const getWorkerResponse = async (messageToWorker: MessageToWorker) => { | |
logger.debug('getWorkerResponse', { messageToWorker }) | |
const worker = new Worker(new URL('./flogram.worker.ts', import.meta.url), { | |
type: 'module', | |
}) | |
const messageFromWorker = await new Promise<MessageFromWorker>(resolve => { | |
worker.onmessage = event => { | |
resolve(event.data) | |
} | |
worker.postMessage(messageToWorker) | |
}) | |
if ('error' in messageFromWorker) { | |
throw messageFromWorker.error | |
} | |
return messageFromWorker.result | |
} | |
const evalExpression = async (toEval: string, context: Map<string, unknown>): Promise<unknown> => { | |
// module import from flogram.ts | |
const messageToWorker: GenericMessageToWorker<MessageTypes.expression> = { | |
toEval, | |
context: context.toJS(), | |
type: MessageTypes.expression, | |
declaredIdentifiers: undefined, | |
} | |
return await getWorkerResponse(messageToWorker) | |
} | |
// returns boolean indicating whether the condition is true in the context | |
export const evalCondition = async ( | |
condition: string, | |
context: Map<string, unknown>, | |
): Promise<boolean> => { | |
return (await evalExpression(`!!(${condition})`, context)) as boolean | |
} | |
export const evalAssignments = async ( | |
toEval: string, | |
context: Map<string, unknown>, | |
): Promise<Map<string, unknown>> => { | |
logger.debug('evalAssignments', { toEval, context }) | |
const messageToWorker: GenericMessageToWorker<MessageTypes.assign> = { | |
type: MessageTypes.assign, | |
toEval, | |
context: context.toJS(), | |
declaredIdentifiers: getDeclaredIdentifiers(toEval), | |
} | |
logger.debug('evalAssignments messageToWorker', messageToWorker) | |
const result = (await getWorkerResponse(messageToWorker)) as { [key: string]: unknown } | |
if (!(result instanceof Object)) { | |
throw new Error(`Expected result to be an object, got ${result}`) | |
} | |
return Map(result) | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import * as esprima from 'esprima' | |
import { ObjectPattern, VariableDeclaration } from 'estree' | |
export const getDeclaredIdentifiers = (toEval: string): string[] => { | |
const ast = esprima.parseModule(toEval) | |
const variableDeclarations = ast.body.filter( | |
node => node.type === 'VariableDeclaration', | |
) as VariableDeclaration[] | |
const assignedIdentifiers = variableDeclarations | |
.map(node => | |
node.declarations.reduce((acc, decl) => { | |
if (decl.id.type === 'Identifier') { | |
acc.push(decl.id.name) | |
} | |
if (decl.id.type === 'ObjectPattern') { | |
acc.push(...getIdentifiersRecursive(decl.id)) | |
} | |
return acc | |
}, [] as string[]), | |
) | |
.flat() | |
return assignedIdentifiers | |
} | |
// recursive function to get all assigned identifiers in a nested destructuring assignment | |
const getIdentifiersRecursive = (ObjectPattern: ObjectPattern): string[] => { | |
const assignedIdentifiers = ObjectPattern.properties.reduce((acc, prop) => { | |
if (prop.type === 'Property' && prop.value.type === 'ObjectPattern') { | |
acc.push(...getIdentifiersRecursive(prop.value)) | |
} else if (prop.type === 'Property' && prop.key.type === 'Identifier') { | |
acc.push(prop.key.name) | |
} | |
return acc | |
}, [] as string[]) | |
return assignedIdentifiers | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
"Flogram" is just some fun terminology we came up with: it just means user-submitted code!