import {produce} from "./producer" import {Dataset, ReadonlyDataset} from "@opennetwork/rdf-dataset"; import {literal} from "@opennetwork/rdf-namespace-javascript" import {isAnswerQuad, isDefaultMain, main} from "../main" import * as ns from "../namespace" import {BlankNode, DefaultDataFactory, isQuadPredicate, NamedNode, Quad} from "@opennetwork/rdf-data-model"; import { run, addEventListener, removeEventListener, getEnvironmentConfig, getEventContext, getEvent, EventContext, dispatchEvent, getDispatcherEvents, setEnvironmentConfig, Environment, setEnvironment, ConfigureEvent, getStore, Event } from "@opennetwork/environment" import {hashDataset} from "./dataset-hash" declare global { interface MainModule { main: typeof main } interface EngineModule { engine: typeof defaultEngine } interface NextEvent extends Event { type: "next" next: ReturnType extends AsyncIterable ? R : never, dataset: ReadonlyDataset } interface EnvironmentEvents { next: NextEvent } interface EnvironmentConfig { uri: string subject: NamedNode | BlankNode dataset: Dataset module: MainModule environment: Environment } } function isMainModule(module: object): module is MainModule { function isLike(module: unknown): module is Record { return !!module } return ( isLike(module) && typeof module.main === "function" ) } function isEngineModule(module: object): module is EngineModule { function isLike(module: unknown): module is Record { return !!module } return ( isLike(module) && typeof module.engine === "function" ) } async function defaultEngine(uri: string): Promise { const dataset = new Dataset() const questions = dataset.match({ predicate: ns.defaultMain.questionPredicate }) const answers = dataset.filter(isAnswerQuad) let module: MainModule, environment: Environment try { addEventListener("configure", configure) await run(createEnvironmentConfig()) } finally { removeEventListener("next", defaultNext) removeEventListener("execute", execute) removeEventListener("configure", configure) removeEventListener("complete", collect) removeEventListener("complete", persist) } function createEnvironmentConfig(partial: Partial = {}): EnvironmentConfig { return { uri, subject: DefaultDataFactory.blankNode(), dataset, get module(): MainModule { if (!module) { throw new Error("Module not yet available") } return module }, get environment(): Environment { if (!environment) { throw new Error("Environment not yet available") } return environment }, ...partial } } async function setPartialEnvironmentConfig(partial: Partial = {}) { await setEnvironmentConfig(createEnvironmentConfig(partial)) } async function configure({ environment: configuredEnvironment}: ConfigureEvent): Promise { if (environment) { throw new Error("Environment already configured!") } environment = configuredEnvironment removeEventListener("configure", configure) const { uri } = getEnvironmentConfig() setEnvironment(() => configuredEnvironment) await setPartialEnvironmentConfig({ uri }) // Importing this module will add all event listeners. This path must be unique per runtime instance // If you want to run multiple engines in the same runtime, create unique paths // This allows developers to create consistent software // // ... or create a new runtime instance per engine module = await import(`${uri}/index.js`) const { subject, dataset } = getEnvironmentConfig() if (!isMainModule(module)) { throw new Error("No MainModule") } await setPartialEnvironmentConfig({ subject, dataset, module, environment }) if (isEngineModule(module) && module.engine !== defaultEngine) { return module.engine(uri) } if (isDefaultMain(module.main)) { addEventListener("next", defaultNext) } // This always happens _after_ all other executes, others can abort! addEventListener("execute", execute) // This should be the final event listener after the above addEventListener("complete", collect) addEventListener("complete", persist) } async function execute() { // We use the environment config dataset, as it may have been changed by the time we get here, e.g. upgraded to an observable async dataset! const { uri: environmentUri, dataset, module, environment } = getEnvironmentConfig() if (environmentUri !== uri) { throw new Error(`Expected uri ${uri}, got ${environmentUri}, please execute one convention engine in a runtime instance at once`) } if (!isMainModule(module)) { throw new Error("No MainModule") } await environment.runInAsyncScope(async () => { const { main } = module await load() try { for await (const next of produce(dataset, main)) { dataset.addAll(next) await persist() const event: NextEvent = { type: "next", next, dataset } await dispatchEvent(event) await persist() } } finally { await persist() } }) } async function collect() { // console.log("collecting time") // console.log(await getStore().get(getDatasetStorageKey())) console.log({ questions: questions.size, answers: answers.size }) // console.log(JSON.stringify(dispatchMap(getDispatcherEvents(getEvent())[0]), undefined, " ")) interface DispatchMap { event: Event, context: EventContext, children: DispatchMap[] } function dispatchMap(event: Event): DispatchMap { const context = getEventContext(event) return { event, context, children: context.dispatchedEvents.map(({ event }) => dispatchMap(event)) } } } async function defaultNext() { // These are our core conventions in practice.. for (const { subject: questionSubject, object: predicate, graph: questionGraph } of questions) { if (!isQuadPredicate(predicate)) { continue } dataset.add( new Quad( questionSubject, predicate, literal("some answer"), questionGraph ) ) } } async function load() { const storage = getStore() const key = getDatasetStorageKey() const stored = await storage.get(key) if (!Array.isArray(stored)) { return } if (dataset.size) { // Everything dataset.delete({}) } dataset.addAll(stored) } async function persist() { const storage = getStore() await hashDataset(dataset) console.log(dataset.toArray()) await storage.set(getDatasetStorageKey(), [...dataset]) } function getDatasetStorageKey() { return `${getSubjectAsString()}:dataset` } function getSubjectAsString() { const { subject } = getEnvironmentConfig() if (subject.termType === "NamedNode") { return subject.value } if (subject.termType !== "BlankNode") { throw new Error("Expected BlankNode or NamedNode for subject") } return `:${subject.value}` } } export const engine = defaultEngine