Skip to content

Instantly share code, notes, and snippets.

@dnsosebee
Created November 17, 2022 04:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save dnsosebee/52e03181bf8c9629a27bc627e11f6412 to your computer and use it in GitHub Desktop.
Save dnsosebee/52e03181bf8c9629a27bc627e11f6412 to your computer and use it in GitHub Desktop.
/* 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(/&lt;/g, '<').replaceAll(/&gt;/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)
}
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)
}
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
}
@dnsosebee
Copy link
Author

"Flogram" is just some fun terminology we came up with: it just means user-submitted code!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment