Serve Sai plugin ABI over CultNet
This commit is contained in:
parent
a5c2a65b0e
commit
2e7be7e9f9
9 changed files with 219 additions and 44 deletions
|
|
@ -10,9 +10,13 @@ behalf, or execute Norn or TeX semantics. It advertises `norn.graph` and
|
||||||
|
|
||||||
## Transport
|
## Transport
|
||||||
|
|
||||||
`src/eve-sidecar.cjs` is a long-running NDJSON process. Each input line is a
|
`src/eve-cultnet-sidecar.cjs` is the runtime-independent service entry point.
|
||||||
`gamecult.eve.plugin_abi.request.v1`; each output line is the correlated
|
It advertises a CultNet RUDP endpoint and accepts `cultnet.operation_request.v0`
|
||||||
`gamecult.eve.plugin_abi.response.v1`.
|
envelopes whose MessagePack payload is a `gamecult.eve.plugin_abi.request.v1`.
|
||||||
|
Responses preserve both the CultNet message correlation and Eve ABI request ID.
|
||||||
|
|
||||||
|
`src/eve-sidecar.cjs` exposes the same handler through newline-delimited JSON
|
||||||
|
for local debugging. It is not the deployment transport.
|
||||||
|
|
||||||
The retained operations are:
|
The retained operations are:
|
||||||
|
|
||||||
|
|
@ -26,6 +30,9 @@ The retained operations are:
|
||||||
|
|
||||||
Run the owner-side conformance witness with:
|
Run the owner-side conformance witness with:
|
||||||
|
|
||||||
```powershell
|
```sh
|
||||||
powershell -ExecutionPolicy Bypass -File scripts/run-eve-plugin-witness.ps1
|
npm run check:eve-plugin
|
||||||
```
|
```
|
||||||
|
|
||||||
|
The witness starts the real CultNet service, invokes every fixture operation
|
||||||
|
through RUDP, and writes `artifacts/eve-plugin/runtime-witness.json`.
|
||||||
|
|
|
||||||
39
package-lock.json
generated
39
package-lock.json
generated
|
|
@ -8,16 +8,51 @@
|
||||||
"name": "@gamecult/sai",
|
"name": "@gamecult/sai",
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@msgpack/msgpack": "^3.1.3",
|
||||||
|
"cultnet-ts": "file:../CultLib/packages/cultnet-ts",
|
||||||
|
"inkjs": "^2.4.0"
|
||||||
|
},
|
||||||
|
"bin": {
|
||||||
|
"sai-eve-sidecar": "src/eve-cultnet-sidecar.cjs",
|
||||||
|
"sai-eve-stdio": "src/eve-sidecar.cjs"
|
||||||
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"inkjs": "^2.4.0",
|
|
||||||
"prettier": "^3.5.3"
|
"prettier": "^3.5.3"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"../CultLib/packages/cultnet-ts": {
|
||||||
|
"version": "0.1.0",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@msgpack/msgpack": "^3.1.3",
|
||||||
|
"ajv": "^8.17.1",
|
||||||
|
"cultcache-ts": "file:../cultcache-ts"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^24.0.0",
|
||||||
|
"json-schema-to-typescript": "^15.0.4",
|
||||||
|
"typescript": "^5.9.0",
|
||||||
|
"zod": "^3.25.76"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@msgpack/msgpack": {
|
||||||
|
"version": "3.1.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/@msgpack/msgpack/-/msgpack-3.1.3.tgz",
|
||||||
|
"integrity": "sha512-47XIizs9XZXvuJgoaJUIE2lFoID8ugvc0jzSHP+Ptfk8nTbnR8g788wv48N03Kx0UkAv559HWRQ3yzOgzlRNUA==",
|
||||||
|
"license": "ISC",
|
||||||
|
"engines": {
|
||||||
|
"node": ">= 18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/cultnet-ts": {
|
||||||
|
"resolved": "../CultLib/packages/cultnet-ts",
|
||||||
|
"link": true
|
||||||
|
},
|
||||||
"node_modules/inkjs": {
|
"node_modules/inkjs": {
|
||||||
"version": "2.4.0",
|
"version": "2.4.0",
|
||||||
"resolved": "https://registry.npmjs.org/inkjs/-/inkjs-2.4.0.tgz",
|
"resolved": "https://registry.npmjs.org/inkjs/-/inkjs-2.4.0.tgz",
|
||||||
"integrity": "sha512-EoPCYESIbMtfI8SqEDZCJwn+A5is0QozMLw250iic1ReJCgZpRKIezWj0VqgRUzAx0f3MmEbsUjY/ILe2815JQ==",
|
"integrity": "sha512-EoPCYESIbMtfI8SqEDZCJwn+A5is0QozMLw250iic1ReJCgZpRKIezWj0VqgRUzAx0f3MmEbsUjY/ILe2815JQ==",
|
||||||
"dev": true,
|
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"bin": {
|
"bin": {
|
||||||
"inkjs-compiler": "bin/inkjs-compiler.js"
|
"inkjs-compiler": "bin/inkjs-compiler.js"
|
||||||
|
|
|
||||||
|
|
@ -22,15 +22,18 @@
|
||||||
"./ink.js": "./dist/ink.js"
|
"./ink.js": "./dist/ink.js"
|
||||||
},
|
},
|
||||||
"bin": {
|
"bin": {
|
||||||
"sai-eve-sidecar": "./src/eve-sidecar.cjs"
|
"sai-eve-sidecar": "./src/eve-cultnet-sidecar.cjs",
|
||||||
|
"sai-eve-stdio": "./src/eve-sidecar.cjs"
|
||||||
},
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build.mjs",
|
"build": "node scripts/build.mjs",
|
||||||
"check": "node scripts/check.mjs",
|
"check": "node scripts/check.mjs",
|
||||||
"check:eve-plugin": "powershell -ExecutionPolicy Bypass -File scripts/run-eve-plugin-witness.ps1",
|
"check:eve-plugin": "node scripts/run-eve-cultnet-witness.mjs",
|
||||||
"compile:example": "inkjs-compiler examples/static-site/story.ink -o examples/static-site/story.ink.json"
|
"compile:example": "inkjs-compiler examples/static-site/story.ink -o examples/static-site/story.ink.json"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@msgpack/msgpack": "^3.1.3",
|
||||||
|
"cultnet-ts": "file:../CultLib/packages/cultnet-ts",
|
||||||
"inkjs": "^2.4.0"
|
"inkjs": "^2.4.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|
|
||||||
|
|
@ -7,17 +7,18 @@
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"invocationModel": "executable-sidecar",
|
"invocationModel": "executable-sidecar",
|
||||||
"contract": "gamecult.eve.plugin_abi.v1",
|
"contract": "gamecult.eve.plugin_abi.v1",
|
||||||
"transports": ["stdio"],
|
"transports": ["cultnet-rudp", "stdio"],
|
||||||
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"],
|
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"],
|
||||||
"sidecar": {
|
"sidecar": {
|
||||||
"processKind": "long-running-daemon",
|
"processKind": "long-running-daemon",
|
||||||
"protocol": "stdio-ndjson",
|
"protocol": "cultnet-operation-v0",
|
||||||
|
"debugProtocol": "stdio-ndjson",
|
||||||
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
|
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
|
||||||
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
|
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
|
||||||
"operations": ["describe", "validate", "open", "apply", "project", "jump"],
|
"operations": ["describe", "validate", "open", "apply", "project", "jump"],
|
||||||
"commandEnvelope": "gamecult.eve.command_invocation.v1",
|
"commandEnvelope": "gamecult.eve.command_invocation.v1",
|
||||||
"receiptSchema": "gamecult.eve.command_receipt.v1",
|
"receiptSchema": "gamecult.eve.command_receipt.v1",
|
||||||
"launch": { "command": "node", "artifact": "src/eve-sidecar.cjs" },
|
"launch": { "command": "node", "artifact": "src/eve-cultnet-sidecar.cjs" },
|
||||||
"stateAuthority": "sai-sidecar-owns-story-session-provider-owns-command-acceptance"
|
"stateAuthority": "sai-sidecar-owns-story-session-provider-owns-command-acceptance"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,12 @@
|
||||||
"runtime": {
|
"runtime": {
|
||||||
"invocationModel": "executable-sidecar",
|
"invocationModel": "executable-sidecar",
|
||||||
"contract": "gamecult.eve.plugin_abi.v1",
|
"contract": "gamecult.eve.plugin_abi.v1",
|
||||||
"transports": ["stdio"],
|
"transports": ["cultnet-rudp", "stdio"],
|
||||||
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"],
|
"authority": ["renderer-independent", "no-provider-state-mutation", "provider-accepts-or-denies-commands"],
|
||||||
"sidecar": {
|
"sidecar": {
|
||||||
"processKind": "long-running-daemon",
|
"processKind": "long-running-daemon",
|
||||||
"protocol": "stdio-ndjson",
|
"protocol": "cultnet-operation-v0",
|
||||||
|
"debugProtocol": "stdio-ndjson",
|
||||||
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
|
"requestSchema": "gamecult.eve.plugin_abi.request.v1",
|
||||||
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
|
"responseSchema": "gamecult.eve.plugin_abi.response.v1",
|
||||||
"operations": ["describe", "validate", "open", "apply", "project", "jump"],
|
"operations": ["describe", "validate", "open", "apply", "project", "jump"],
|
||||||
|
|
|
||||||
91
scripts/run-eve-cultnet-witness.mjs
Normal file
91
scripts/run-eve-cultnet-witness.mjs
Normal file
|
|
@ -0,0 +1,91 @@
|
||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { spawn } from "node:child_process";
|
||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import readline from "node:readline";
|
||||||
|
import { Buffer } from "node:buffer";
|
||||||
|
import { decode, encode } from "@msgpack/msgpack";
|
||||||
|
import { invokeCultNetOperation } from "cultnet-ts";
|
||||||
|
|
||||||
|
const root = path.resolve(import.meta.dirname, "..");
|
||||||
|
const fixturePath = path.join(root, "plugins/sai-vn.plugin-abi-fixture.json");
|
||||||
|
const fixture = JSON.parse(fs.readFileSync(fixturePath, "utf8"));
|
||||||
|
const sidecarPath = path.join(root, "src/eve-cultnet-sidecar.cjs");
|
||||||
|
const child = spawn(process.execPath, [sidecarPath], { cwd: root, stdio: ["ignore", "pipe", "inherit"] });
|
||||||
|
const lines = readline.createInterface({ input: child.stdout })[Symbol.asyncIterator]();
|
||||||
|
const endpointLine = await lines.next();
|
||||||
|
assert.equal(endpointLine.done, false);
|
||||||
|
const endpointAdvertisement = JSON.parse(endpointLine.value);
|
||||||
|
assert.equal(endpointAdvertisement.pluginId, fixture.pluginId);
|
||||||
|
const startedAt = performance.now();
|
||||||
|
const operations = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let index = 0; index < fixture.operations.length; index += 1) {
|
||||||
|
const fixtureOperation = fixture.operations[index];
|
||||||
|
const requestId = `sai-witness-${index}`;
|
||||||
|
const operationStartedAt = performance.now();
|
||||||
|
const abiRequest = {
|
||||||
|
schema: fixture.requestSchema,
|
||||||
|
pluginId: fixture.pluginId,
|
||||||
|
operation: fixtureOperation.operation,
|
||||||
|
requestId,
|
||||||
|
input: fixtureOperation.input,
|
||||||
|
};
|
||||||
|
const envelope = await invokeCultNetOperation(endpointAdvertisement.endpoint, {
|
||||||
|
schemaVersion: "cultnet.operation_request.v0",
|
||||||
|
messageId: requestId,
|
||||||
|
serviceId: fixture.pluginId,
|
||||||
|
operation: fixtureOperation.operation,
|
||||||
|
payloadSchema: fixture.requestSchema,
|
||||||
|
payloadEncoding: "messagepack-base64",
|
||||||
|
payload: Buffer.from(encode(abiRequest)).toString("base64"),
|
||||||
|
sourceRuntimeId: "eve-conformance",
|
||||||
|
}, { runtimeId: "eve-conformance" });
|
||||||
|
const response = decode(Buffer.from(envelope.payload, "base64"));
|
||||||
|
assert.equal(response.schema, fixture.responseSchema);
|
||||||
|
assert.equal(response.pluginId, fixture.pluginId);
|
||||||
|
assert.equal(response.operation, fixtureOperation.operation);
|
||||||
|
assert.equal(response.requestId, requestId);
|
||||||
|
assert.equal(response.status, "accepted");
|
||||||
|
assertSubset(response.output, fixtureOperation.expect, fixtureOperation.operation);
|
||||||
|
operations.push({
|
||||||
|
operation: fixtureOperation.operation,
|
||||||
|
requestId,
|
||||||
|
status: response.status,
|
||||||
|
durationMs: Number((performance.now() - operationStartedAt).toFixed(3)),
|
||||||
|
expectationCount: Object.keys(fixtureOperation.expect).length,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
child.kill();
|
||||||
|
}
|
||||||
|
|
||||||
|
const outputPath = path.join(root, "artifacts/eve-plugin/runtime-witness.json");
|
||||||
|
fs.mkdirSync(path.dirname(outputPath), { recursive: true });
|
||||||
|
fs.writeFileSync(outputPath, `${JSON.stringify({
|
||||||
|
schema: "gamecult.eve.plugin_witness.v1",
|
||||||
|
witnessId: "sai.vn.owner-sidecar",
|
||||||
|
pluginId: fixture.pluginId,
|
||||||
|
ownerRepo: "Sai",
|
||||||
|
status: "pass",
|
||||||
|
transport: "cultnet-operation-v0+rudp",
|
||||||
|
generatedAtUtc: new Date().toISOString(),
|
||||||
|
durationMs: Number((performance.now() - startedAt).toFixed(3)),
|
||||||
|
operations,
|
||||||
|
advertisementPath: "plugins/sai-vn.plugin-advertisement.json",
|
||||||
|
fixturePath: "plugins/sai-vn.plugin-abi-fixture.json",
|
||||||
|
executable: { artifact: "src/eve-cultnet-sidecar.cjs", command: "node" },
|
||||||
|
diagnostics: [],
|
||||||
|
authority: "sai-sidecar-owns-story-session-provider-retains-command-acceptance",
|
||||||
|
}, null, 2)}\n`);
|
||||||
|
console.log(outputPath);
|
||||||
|
|
||||||
|
function assertSubset(actual, expected, label) {
|
||||||
|
if (Array.isArray(expected)) return assert.deepEqual(actual, expected, label);
|
||||||
|
if (expected && typeof expected === "object") {
|
||||||
|
for (const [key, value] of Object.entries(expected)) assertSubset(actual?.[key], value, `${label}.${key}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
assert.equal(actual, expected, label);
|
||||||
|
}
|
||||||
|
|
@ -1,22 +1,6 @@
|
||||||
param(
|
param([string] $NodePath = "node")
|
||||||
[string] $EveRoot = "E:\Projects\Eve",
|
|
||||||
[string] $OutputPath = "artifacts\eve-plugin\runtime-witness.json",
|
|
||||||
[string] $NodePath = "node"
|
|
||||||
)
|
|
||||||
|
|
||||||
$ErrorActionPreference = "Stop"
|
$ErrorActionPreference = "Stop"
|
||||||
$repoRoot = Split-Path -Parent $PSScriptRoot
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||||
$output = if ([IO.Path]::IsPathRooted($OutputPath)) { $OutputPath } else { Join-Path $repoRoot $OutputPath }
|
& $NodePath (Join-Path $PSScriptRoot "run-eve-cultnet-witness.mjs")
|
||||||
$launcher = Join-Path $EveRoot "tools\plugins\run-sidecar-witness.mjs"
|
|
||||||
|
|
||||||
& $NodePath $launcher `
|
|
||||||
--advertisement (Join-Path $repoRoot "plugins\sai-vn.plugin-advertisement.json") `
|
|
||||||
--fixture (Join-Path $repoRoot "plugins\sai-vn.plugin-abi-fixture.json") `
|
|
||||||
--output $output `
|
|
||||||
--witness-id "sai.vn.owner-sidecar" `
|
|
||||||
--cwd $repoRoot `
|
|
||||||
--command $NodePath `
|
|
||||||
-- "src/eve-sidecar.cjs"
|
|
||||||
if ($LASTEXITCODE -ne 0) { throw "Sai Eve plugin witness failed with exit code $LASTEXITCODE" }
|
if ($LASTEXITCODE -ne 0) { throw "Sai Eve plugin witness failed with exit code $LASTEXITCODE" }
|
||||||
|
|
||||||
Write-Host "Sai Eve plugin witness: $output"
|
|
||||||
|
|
|
||||||
45
src/eve-cultnet-sidecar.cjs
Normal file
45
src/eve-cultnet-sidecar.cjs
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
const { Buffer } = require("node:buffer");
|
||||||
|
const { decode, encode } = require("@msgpack/msgpack");
|
||||||
|
const { createResponse, dispatch, pluginId } = require("./eve-sidecar.cjs");
|
||||||
|
|
||||||
|
(async () => {
|
||||||
|
const { startCultNetOperationServer } = await import("cultnet-ts");
|
||||||
|
const server = await startCultNetOperationServer({
|
||||||
|
runtimeId: "sai-vn-plugin",
|
||||||
|
host: process.env.EVE_PLUGIN_HOST || "127.0.0.1",
|
||||||
|
port: Number(process.env.EVE_PLUGIN_PORT || 0),
|
||||||
|
handler: request => {
|
||||||
|
if (request.serviceId !== pluginId) throw new Error(`Expected service ${pluginId}.`);
|
||||||
|
if (request.payloadSchema !== "gamecult.eve.plugin_abi.request.v1") throw new Error("Unsupported plugin payload schema.");
|
||||||
|
const abiRequest = decode(Buffer.from(request.payload, "base64"));
|
||||||
|
let abiResponse;
|
||||||
|
try {
|
||||||
|
abiResponse = createResponse(abiRequest, dispatch(abiRequest));
|
||||||
|
} catch (error) {
|
||||||
|
abiResponse = createResponse(abiRequest, {}, "rejected", error instanceof Error ? error.message : String(error));
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
schemaVersion: "cultnet.operation_response.v0",
|
||||||
|
messageId: request.messageId,
|
||||||
|
serviceId: pluginId,
|
||||||
|
operation: request.operation,
|
||||||
|
status: abiResponse.status,
|
||||||
|
payloadSchema: abiResponse.schema,
|
||||||
|
payloadEncoding: "messagepack-base64",
|
||||||
|
payload: Buffer.from(encode(abiResponse)).toString("base64"),
|
||||||
|
diagnostics: (abiResponse.diagnostics || []).map(item => item.message),
|
||||||
|
sourceRuntimeId: "sai-vn-plugin",
|
||||||
|
};
|
||||||
|
},
|
||||||
|
});
|
||||||
|
process.stdout.write(`${JSON.stringify({ schema: "gamecult.eve.plugin_endpoint.v1", pluginId, endpoint: server.endpoint })}\n`);
|
||||||
|
const stop = async () => { await server.close(); process.exit(0); };
|
||||||
|
process.once("SIGINT", stop);
|
||||||
|
process.once("SIGTERM", stop);
|
||||||
|
await new Promise(() => {});
|
||||||
|
})().catch(error => {
|
||||||
|
process.stderr.write(`${error.stack || error}\n`);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
|
@ -10,16 +10,18 @@ const sessions = new Map();
|
||||||
const pluginId = "sai.vn";
|
const pluginId = "sai.vn";
|
||||||
const operations = ["describe", "validate", "open", "apply", "project", "jump"];
|
const operations = ["describe", "validate", "open", "apply", "project", "jump"];
|
||||||
|
|
||||||
readline.createInterface({ input: process.stdin }).on("line", (line) => {
|
if (require.main === module) {
|
||||||
if (!line.trim()) return;
|
readline.createInterface({ input: process.stdin }).on("line", (line) => {
|
||||||
let request;
|
if (!line.trim()) return;
|
||||||
try {
|
let request;
|
||||||
request = JSON.parse(line.replace(/^\uFEFF/, ""));
|
try {
|
||||||
respond(request, dispatch(request));
|
request = JSON.parse(line.replace(/^\uFEFF/, ""));
|
||||||
} catch (error) {
|
respond(request, dispatch(request));
|
||||||
respond(request, {}, "rejected", error.message);
|
} catch (error) {
|
||||||
}
|
respond(request, {}, "rejected", error.message);
|
||||||
});
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function dispatch(request) {
|
function dispatch(request) {
|
||||||
if (request?.schema !== "gamecult.eve.plugin_abi.request.v1") throw new Error("Unsupported request schema.");
|
if (request?.schema !== "gamecult.eve.plugin_abi.request.v1") throw new Error("Unsupported request schema.");
|
||||||
|
|
@ -156,7 +158,11 @@ function requiredString(value, field) {
|
||||||
function parseJson(value) { return JSON.parse(value.replace(/^\uFEFF/, "")); }
|
function parseJson(value) { return JSON.parse(value.replace(/^\uFEFF/, "")); }
|
||||||
|
|
||||||
function respond(request, output, status = "accepted", message) {
|
function respond(request, output, status = "accepted", message) {
|
||||||
process.stdout.write(`${JSON.stringify({
|
process.stdout.write(`${JSON.stringify(createResponse(request, output, status, message))}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createResponse(request, output, status = "accepted", message) {
|
||||||
|
return {
|
||||||
schema: "gamecult.eve.plugin_abi.response.v1",
|
schema: "gamecult.eve.plugin_abi.response.v1",
|
||||||
pluginId,
|
pluginId,
|
||||||
operation: request?.operation || "unknown",
|
operation: request?.operation || "unknown",
|
||||||
|
|
@ -164,5 +170,7 @@ function respond(request, output, status = "accepted", message) {
|
||||||
status,
|
status,
|
||||||
output,
|
output,
|
||||||
...(message ? { diagnostics: [{ message }] } : {}),
|
...(message ? { diagnostics: [{ message }] } : {}),
|
||||||
})}\n`);
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
module.exports = { createResponse, dispatch, operations, pluginId };
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue