├── .nvmrc
├── .node-version
├── src
├── GEN_VERSION
├── version.ts
├── nodejs
│ ├── index.ts
│ └── polyfill.ts
├── sb.ts
├── Error.ts
├── idGenerator.ts
├── SubstrateResponse.ts
├── index.ts
├── Streaming.ts
├── SubstrateStreamingResponse.ts
├── Platform.ts
├── EventSource.ts
├── Node.ts
├── Future.ts
└── Substrate.ts
├── GEN_VERSION
├── .pre-commit-config.yaml
├── examples
├── streaming
│ ├── nextjs-basic
│ │ ├── next.config.mjs
│ │ ├── app
│ │ │ ├── favicon.ico
│ │ │ ├── page.tsx
│ │ │ ├── globals.css
│ │ │ ├── layout.tsx
│ │ │ ├── api
│ │ │ │ └── generate-text
│ │ │ │ │ └── route.ts
│ │ │ └── Demo.tsx
│ │ ├── postcss.config.mjs
│ │ ├── .gitignore
│ │ ├── package.json
│ │ ├── tailwind.config.ts
│ │ ├── public
│ │ │ ├── vercel.svg
│ │ │ └── next.svg
│ │ ├── tsconfig.json
│ │ └── README.md
│ ├── nextjs-multiple-nodes
│ │ ├── next.config.mjs
│ │ ├── app
│ │ │ ├── favicon.ico
│ │ │ ├── page.tsx
│ │ │ ├── globals.css
│ │ │ ├── layout.tsx
│ │ │ ├── Demo.tsx
│ │ │ └── api
│ │ │ │ └── this-or-that
│ │ │ │ └── route.ts
│ │ ├── postcss.config.mjs
│ │ ├── .gitignore
│ │ ├── tailwind.config.ts
│ │ ├── public
│ │ │ ├── vercel.svg
│ │ │ └── next.svg
│ │ ├── package.json
│ │ ├── tsconfig.json
│ │ └── README.md
│ └── node-http-basic
│ │ ├── package.json
│ │ ├── README.md
│ │ ├── server.js
│ │ └── package-lock.json
├── basic.js
├── basic.cjs
├── qa.ts
├── streaming.ts
├── large-run.ts
├── string-concat.ts
├── jina.ts
├── logo-controlnet.ts
├── basic.ts
├── string-interpolation.ts
├── implicit-nodes.ts
├── jq.ts
├── box.ts
├── image-generation.ts
├── json.ts
├── mixture-of-agents
│ ├── util.ts
│ ├── ask.ts
│ └── index.html
├── knowledge-graph
│ ├── util.ts
│ ├── generate.ts
│ └── index.html
├── descript
│ ├── generate.ts
│ ├── util.ts
│ ├── generate-chapters.ts
│ └── index.html
├── if.ts
├── explicit-edges.ts
├── vector-store.ts
└── kitchen-sink.ts
├── .gitignore
├── tsup.config.ts
├── vitest.config.ts
├── bin
├── sync-codegen.ts
└── update-version.ts
├── tests
├── Nodes.test.ts
├── Node.test.ts
├── SubstrateResponse.test.ts
├── Substrate.test.ts
└── Future.test.ts
├── .github
└── workflows
│ └── node.js.yml
├── LICENSE.txt
├── package.json
├── Makefile
├── README.md
├── tsconfig.json
└── DEVELOPMENT.md
/.nvmrc:
--------------------------------------------------------------------------------
1 | 18.17.1
2 |
--------------------------------------------------------------------------------
/.node-version:
--------------------------------------------------------------------------------
1 | 18.17.1
2 |
--------------------------------------------------------------------------------
/src/GEN_VERSION:
--------------------------------------------------------------------------------
1 | 20240617.20240815
--------------------------------------------------------------------------------
/GEN_VERSION:
--------------------------------------------------------------------------------
1 | 20240313.20240313225414
--------------------------------------------------------------------------------
/src/version.ts:
--------------------------------------------------------------------------------
1 | export const VERSION = "120240617.1.9";
2 |
--------------------------------------------------------------------------------
/src/nodejs/index.ts:
--------------------------------------------------------------------------------
1 | // entrypoint for nodejs build
2 | import "substrate/nodejs/polyfill";
3 | export * from "..";
4 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | - repo: https://github.com/pre-commit/mirrors-prettier
3 | rev: "v3.1.0"
4 | hooks:
5 | - id: prettier
6 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zocomputer/substrate-typescript/HEAD/examples/streaming/nextjs-basic/app/favicon.ico
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/next.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('next').NextConfig} */
2 | const nextConfig = {};
3 |
4 | export default nextConfig;
5 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/app/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/zocomputer/substrate-typescript/HEAD/examples/streaming/nextjs-multiple-nodes/app/favicon.ico
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | /** @type {import('postcss-load-config').Config} */
2 | const config = {
3 | plugins: {
4 | tailwindcss: {},
5 | },
6 | };
7 |
8 | export default config;
9 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # build
2 | dist/
3 |
4 | # dependencies
5 | node_modules/
6 |
7 | # logs, dev environment
8 | npm-debug.log
9 | .DS_Store
10 | tmp/
11 |
12 | # scratch
13 | examples/scratch.ts
14 | **/moa.html
15 | **/descript.html
16 | **/knowledge-graph.html
17 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Demo from "./Demo";
2 |
3 | export default function Home() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/app/page.tsx:
--------------------------------------------------------------------------------
1 | import Demo from "./Demo";
2 |
3 | export default function Home() {
4 | return (
5 |
6 |
7 |
8 | );
9 | }
10 |
--------------------------------------------------------------------------------
/tsup.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "tsup";
2 |
3 | export default defineConfig({
4 | entry: ["src/index.ts", "src/nodejs/index.ts"],
5 | format: ["cjs", "esm"],
6 | dts: true,
7 | keepNames: true,
8 | cjsInterop: true,
9 | splitting: true,
10 | minify: false,
11 | clean: true,
12 | sourcemap: true,
13 | });
14 |
--------------------------------------------------------------------------------
/examples/streaming/node-http-basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node-http-basic",
3 | "version": "1.0.0",
4 | "description": "",
5 | "type": "module",
6 | "main": "server.js",
7 | "scripts": {
8 | "start": "node server.js"
9 | },
10 | "keywords": [],
11 | "author": "",
12 | "license": "ISC",
13 | "dependencies": {}
14 | }
15 |
--------------------------------------------------------------------------------
/vitest.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 | import tsconfigPaths from "vite-tsconfig-paths";
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [tsconfigPaths()],
7 | test: {
8 | environment: "node",
9 | include: ["**/*.test.ts"],
10 | exclude: ["node_modules/**"],
11 | },
12 | });
13 |
--------------------------------------------------------------------------------
/src/sb.ts:
--------------------------------------------------------------------------------
1 | import { FutureAnyObject, FutureString } from "substrate/Future";
2 | import { StreamingResponse } from "substrate/SubstrateStreamingResponse";
3 |
4 | export const sb = {
5 | concat: FutureString.concat,
6 | jq: FutureAnyObject.jq,
7 | interpolate: FutureString.interpolate,
8 | streaming: {
9 | fromSSEResponse: StreamingResponse.fromReponse,
10 | },
11 | };
12 |
--------------------------------------------------------------------------------
/bin/sync-codegen.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { execSync } from "node:child_process";
4 |
5 | const ok = (message: string) => console.log("\x1b[32m✓\x1b[0m", message);
6 | const DIR = "../substrate/codegen/typescript";
7 |
8 | execSync(`cp -r ${DIR}/src/* src/`);
9 | execSync(`cp ../substrate/site/public/openapi.json src/openapi.json`);
10 | ok(`Copied generated code from ${DIR}`);
11 |
--------------------------------------------------------------------------------
/tests/Nodes.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, describe, test } from "vitest";
2 | import { ComputeText } from "substrate/Nodes";
3 |
4 | describe("ComputeText", () => {
5 | test(".node", () => {
6 | const n = new ComputeText({ prompt: "foo" });
7 | expect(n.node).toEqual("ComputeText");
8 | });
9 |
10 | test(".future", () => {
11 | const a = new ComputeText({ prompt: "foo" });
12 | const b = new ComputeText({ prompt: a.future.text });
13 | expect(b).toBeInstanceOf(ComputeText);
14 | });
15 | });
16 |
--------------------------------------------------------------------------------
/src/nodejs/polyfill.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * While we're generally aiming to support ES2022 and Node 18+ we're also including
3 | * polyfill code for now for some of the Standard Web APIs that we use in this SDK.
4 | */
5 | import fetch, { Headers, Request, Response } from "node-fetch";
6 |
7 | if (!globalThis.fetch) {
8 | // @ts-ignore
9 | globalThis.fetch = fetch;
10 | // @ts-ignore
11 | globalThis.Headers = Headers;
12 | // @ts-ignore
13 | globalThis.Request = Request;
14 | // @ts-ignore
15 | globalThis.Response = Response;
16 | }
17 |
--------------------------------------------------------------------------------
/src/Error.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Errors thrown by the Substrate SDK will be instances of `SubstrateError`.
3 | */
4 | export class SubstrateError extends Error {}
5 |
6 | export class RequestTimeoutError extends SubstrateError {}
7 |
8 | export class NodeError extends SubstrateError {
9 | type: string;
10 | request_id?: string;
11 | override message: string;
12 |
13 | constructor(type: string, message: string, request_id?: string) {
14 | super(message);
15 | this.type = type;
16 | this.message = message;
17 | this.request_id = request_id;
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/examples/basic.js:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | import { Substrate, ComputeText } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const a = new ComputeText({
11 | prompt: "ask me a short trivia question in one sentence",
12 | });
13 | const b = new ComputeText({ prompt: a.future.text });
14 |
15 | const res = await substrate.run(a, b);
16 |
17 | console.log({ a: res.get(a), b: res.get(b) });
18 | }
19 | main();
20 |
--------------------------------------------------------------------------------
/examples/basic.cjs:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 |
3 | const { Substrate, ComputeText } = require("substrate");
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const a = new ComputeText({
11 | prompt: "ask me a short trivia question in one sentence",
12 | });
13 | const b = new ComputeText({ prompt: a.future.text });
14 |
15 | const res = await substrate.run(a, b);
16 |
17 | console.log({ a: res.get(a), b: res.get(b) });
18 | }
19 |
20 | main();
21 |
--------------------------------------------------------------------------------
/examples/qa.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, Box, sb } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const node = new Box({
11 | value: {
12 | a: "b",
13 | c: {
14 | d: [1, 2, 3],
15 | },
16 | },
17 | });
18 | const res = await substrate.run(node);
19 | console.log(res.apiResponse.status);
20 | console.log(JSON.stringify(res.json));
21 | }
22 | main();
23 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | body {
12 | color: rgb(var(--foreground-rgb));
13 | background: linear-gradient(
14 | to bottom,
15 | transparent,
16 | rgb(var(--background-end-rgb))
17 | )
18 | rgb(var(--background-start-rgb));
19 | }
20 |
21 | @layer utilities {
22 | .text-balance {
23 | text-wrap: balance;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "web",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "react": "^18",
13 | "react-dom": "^18",
14 | "next": "14.2.3"
15 | },
16 | "devDependencies": {
17 | "typescript": "^5",
18 | "@types/node": "^20",
19 | "@types/react": "^18",
20 | "@types/react-dom": "^18",
21 | "postcss": "^8",
22 | "tailwindcss": "^3.4.1"
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 | .yarn/install-state.gz
8 |
9 | # testing
10 | /coverage
11 |
12 | # next.js
13 | /.next/
14 | /out/
15 |
16 | # production
17 | /build
18 |
19 | # misc
20 | .DS_Store
21 | *.pem
22 |
23 | # debug
24 | npm-debug.log*
25 | yarn-debug.log*
26 | yarn-error.log*
27 |
28 | # local env files
29 | .env*.local
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/app/globals.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | :root {
6 | --foreground-rgb: 0, 0, 0;
7 | --background-start-rgb: 214, 219, 220;
8 | --background-end-rgb: 255, 255, 255;
9 | }
10 |
11 | body {
12 | color: rgb(var(--foreground-rgb));
13 | background: linear-gradient(
14 | to bottom,
15 | transparent,
16 | rgb(var(--background-end-rgb))
17 | )
18 | rgb(var(--background-start-rgb));
19 | }
20 |
21 | @layer utilities {
22 | .text-balance {
23 | text-wrap: balance;
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 |
7 | export const metadata: Metadata = {
8 | title: "Create Next App",
9 | description: "Generated by create next app",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: Readonly<{
15 | children: React.ReactNode;
16 | }>) {
17 | return (
18 |
19 |
{children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import type { Metadata } from "next";
2 | import { Inter } from "next/font/google";
3 | import "./globals.css";
4 |
5 | const inter = Inter({ subsets: ["latin"] });
6 |
7 | export const metadata: Metadata = {
8 | title: "Create Next App",
9 | description: "Generated by create next app",
10 | };
11 |
12 | export default function RootLayout({
13 | children,
14 | }: Readonly<{
15 | children: React.ReactNode;
16 | }>) {
17 | return (
18 |
19 | {children}
20 |
21 | );
22 | }
23 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
13 | "gradient-conic":
14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
15 | },
16 | },
17 | },
18 | plugins: [],
19 | };
20 | export default config;
21 |
--------------------------------------------------------------------------------
/examples/streaming/node-http-basic/README.md:
--------------------------------------------------------------------------------
1 | # Basic NodeJS server
2 |
3 | Example using a NodeJS server that accepts a `prompt` query param and responds with a Server-Sent Event stream.
4 |
5 | ## Setup
6 |
7 | I'm using my local package for running this example using `npm link`.
8 |
9 | ```
10 | # in the project root
11 | nvm use v16.18.1
12 | npm link
13 |
14 | # in this example directory
15 | npm link substrate
16 | ```
17 |
18 | ## Running the example
19 |
20 | ```
21 | # run the server
22 | npm start
23 |
24 | # query the server
25 | curl --get http://localhost:3000 --data-urlencode "prompt=tell me about AI"
26 | ```
27 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/tailwind.config.ts:
--------------------------------------------------------------------------------
1 | import type { Config } from "tailwindcss";
2 |
3 | const config: Config = {
4 | content: [
5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}",
6 | "./components/**/*.{js,ts,jsx,tsx,mdx}",
7 | "./app/**/*.{js,ts,jsx,tsx,mdx}",
8 | ],
9 | theme: {
10 | extend: {
11 | backgroundImage: {
12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))",
13 | "gradient-conic":
14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))",
15 | },
16 | },
17 | },
18 | plugins: [],
19 | };
20 | export default config;
21 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/streaming.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, Llama3Instruct70B } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const a = new Llama3Instruct70B({
11 | prompt: "what are server side events useful for?",
12 | max_tokens: 50,
13 | });
14 |
15 | const stream = await substrate.stream(a);
16 |
17 | for await (let message of stream.get(a)) {
18 | if (message.object === "node.delta") {
19 | console.log(message);
20 | }
21 | }
22 | }
23 | main();
24 |
--------------------------------------------------------------------------------
/examples/large-run.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, ComputeText } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | let nodes = [];
11 | let prompt: any = "once upon a time...";
12 | for (let i = 0; i < 50; i++) {
13 | const node = new ComputeText({ prompt });
14 | nodes.push(node);
15 | prompt = node.future.text.concat(" and then");
16 | }
17 |
18 | const res = await substrate.run(...nodes);
19 |
20 | console.log(JSON.stringify(res.json, null, 2));
21 | }
22 | main();
23 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "nextjs-multiple-nodes",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev",
7 | "build": "next build",
8 | "start": "next start",
9 | "lint": "next lint"
10 | },
11 | "dependencies": {
12 | "next": "14.2.3",
13 | "react": "^18",
14 | "react-dom": "^18",
15 | "zod": "^3.23.8",
16 | "zod-to-json-schema": "^3.23.0"
17 | },
18 | "devDependencies": {
19 | "@types/node": "^20",
20 | "@types/react": "^18",
21 | "@types/react-dom": "^18",
22 | "postcss": "^8",
23 | "tailwindcss": "^3.4.1",
24 | "typescript": "^5"
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/examples/string-concat.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, ComputeText, sb } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const a = new ComputeText({
11 | prompt: "name a random capital city: , ",
12 | });
13 |
14 | const concatenated = sb.concat("tell me about visiting ", a.future.text);
15 |
16 | const b = new ComputeText({ prompt: concatenated });
17 |
18 | const res = await substrate.run(a, b);
19 |
20 | console.log({ a: res.get(a), b: res.get(b) });
21 | }
22 | main();
23 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/src/idGenerator.ts:
--------------------------------------------------------------------------------
1 | export function randomString(length: number): string {
2 | const alphabet: string =
3 | "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_-";
4 | let randomString: string = "";
5 | for (let i = 0; i < length; i++) {
6 | const randomIndex: number = Math.floor(Math.random() * alphabet.length);
7 | randomString += alphabet[randomIndex];
8 | }
9 | return randomString;
10 | }
11 |
12 | // Generates incrementing ids, for better legibility
13 | export function idGenerator(prefix: string, start: number = 1): any {
14 | let n = start;
15 | return () => {
16 | const id = `${prefix}${n.toString()}_${randomString(8)}`;
17 | n = n + 1;
18 | return id;
19 | };
20 | }
21 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "lib": ["dom", "dom.iterable", "esnext"],
4 | "allowJs": true,
5 | "skipLibCheck": true,
6 | "strict": true,
7 | "noEmit": true,
8 | "esModuleInterop": true,
9 | "module": "esnext",
10 | "moduleResolution": "bundler",
11 | "resolveJsonModule": true,
12 | "isolatedModules": true,
13 | "jsx": "preserve",
14 | "incremental": true,
15 | "plugins": [
16 | {
17 | "name": "next"
18 | }
19 | ],
20 | "paths": {
21 | "@/*": ["./*"]
22 | }
23 | },
24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
25 | "exclude": ["node_modules"]
26 | }
27 |
--------------------------------------------------------------------------------
/examples/jina.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, ComputeText, JinaV2 } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const a = new ComputeText({ prompt: "hi" });
11 |
12 | const input: JinaV2.Input = {
13 | items: [
14 | {
15 | text: a.future.text,
16 | doc_id: "doc_id",
17 | metadata: {
18 | updated_at: "updated_at",
19 | },
20 | },
21 | ],
22 | };
23 |
24 | const b = new JinaV2(input);
25 |
26 | const res = await substrate.run(b);
27 |
28 | console.log(res.json);
29 | }
30 | main();
31 |
--------------------------------------------------------------------------------
/examples/logo-controlnet.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, StableDiffusionXLControlNet } from "substrate";
4 |
5 | async function main() {
6 | const substrate = new Substrate({ apiKey: process.env["SUBSTRATE_API_KEY"] });
7 | const image_uri = "https://media.substrate.run/logo-nopad-bg-white.png";
8 | const controlnet = new StableDiffusionXLControlNet({
9 | prompt: "sunlit bright birds-eye view of the ocean, turbulent choppy waves",
10 | image_uri: image_uri,
11 | control_method: "illusion",
12 | num_images: 4,
13 | store: "hosted",
14 | conditioning_scale: 1,
15 | });
16 | const res = await substrate.run(controlnet);
17 | console.log(res.get(controlnet));
18 | }
19 | main();
20 |
--------------------------------------------------------------------------------
/examples/basic.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, ComputeText, sb } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const story = new ComputeText({ prompt: "tell me a story" });
11 | const summary = new ComputeText({
12 | prompt: sb.interpolate`summarize this story in one sentence: ${story.future.text}`,
13 | });
14 |
15 | const res = await substrate.run(story, summary);
16 |
17 | const summaryOut = res.get(summary);
18 | console.log(summaryOut.text);
19 |
20 | const visualize = Substrate.visualize(story, summary);
21 | console.log(visualize);
22 | }
23 | main();
24 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/app/api/generate-text/route.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { Substrate, Llama3Instruct70B } from "substrate";
4 |
5 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
6 |
7 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
8 |
9 | export async function POST(request: Request) {
10 | const formData = await request.formData();
11 | const prompt =
12 | formData.get("prompt")?.toString() ||
13 | "write a short error message that informs a user they should fill in the prompt field";
14 |
15 | const node = new Llama3Instruct70B({ prompt });
16 |
17 | const streamResponse = await substrate.stream(node);
18 | const body = streamResponse.apiResponse.body;
19 | return new Response(body, {
20 | headers: { "Content-Type": "text/event-stream" },
21 | });
22 | }
23 |
--------------------------------------------------------------------------------
/examples/string-interpolation.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, ComputeText, sb } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const concise =
11 | "(just give me the number, no punctuation, no empty spaces, no other text)";
12 |
13 | const a = new ComputeText({
14 | prompt: `pick a random number between 1 and 100 ${concise}`,
15 | });
16 |
17 | const b = new ComputeText({
18 | prompt: sb.interpolate`double the following number: ${a.future.text} ${concise}`,
19 | });
20 |
21 | const res = await substrate.run(a, b);
22 |
23 | console.log({
24 | a: res.get(a),
25 | b: res.get(b),
26 | });
27 | }
28 | main();
29 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/README.md:
--------------------------------------------------------------------------------
1 | # Simple NextJS example
2 |
3 | This example uses a NextJS app that makes a request to Substrate in a route handler and sends the Server-Sent Events directly back to the client.
4 |
5 | On the client we can use a helper to decode these messsages into JavaScript objects that can be used to render out the content as it arrives.
6 |
7 | ## Setup
8 |
9 | I'm using my local package for running this example using `npm link`.
10 |
11 | ```
12 | # in the project root
13 | nvm use
14 | npm link
15 |
16 | # in this example directory
17 | npm link substrate
18 | ```
19 |
20 | ## Running the example
21 |
22 | ```
23 | # install the dependencies
24 | npm install
25 |
26 | # run the dev server
27 | npm run dev
28 |
29 | # open your browser to use it (on localhost:3000 by default)
30 | open http://localhost:3000
31 | ```
32 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/README.md:
--------------------------------------------------------------------------------
1 | # Streaming outputs from multiple nodes
2 |
3 | This example uses a NextJS app that makes a request to Substrate in a route handler, processes the events, and streams back new SSE events back to the user.
4 |
5 | This example demonstrates recieving the results from different nodes in the a graph run as they complete.
6 |
7 | On the client we can use a helper to decode these messsages into JavaScript objects that can be used to render out the content as it arrives.
8 |
9 | ## Setup
10 |
11 | I'm using my local package for running this example using `npm link`.
12 |
13 | ```
14 | # in the project root
15 | nvm use
16 | npm link
17 |
18 | # in this example directory
19 | npm link substrate
20 | ```
21 |
22 | ## Running the example
23 |
24 | ```
25 | # install the dependencies
26 | npm install
27 |
28 | # run the dev server
29 | npm run dev
30 |
31 | # open your browser to use it (on localhost:3000 by default)
32 | open http://localhost:3000
33 | ```
34 |
--------------------------------------------------------------------------------
/examples/implicit-nodes.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, ComputeText } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const a = new ComputeText(
11 | { prompt: "tell me about windmills", max_tokens: 10 },
12 | { id: "a" },
13 | );
14 | const b = new ComputeText(
15 | { prompt: a.future.text, max_tokens: 10 },
16 | { id: "b" },
17 | );
18 | const c = new ComputeText(
19 | { prompt: b.future.text, max_tokens: 10 },
20 | { id: "c" },
21 | );
22 | const d = new ComputeText(
23 | { prompt: c.future.text, max_tokens: 10 },
24 | { id: "d" },
25 | );
26 |
27 | // Because the `c` is the the final node in the graph we can find nodes it depends
28 | // on through the relationships created via the input arguments.
29 | const res = await substrate.run(d);
30 | console.log(res.json);
31 | }
32 | main();
33 |
--------------------------------------------------------------------------------
/examples/jq.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, ComputeText, sb, ComputeJSON } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const a = new ComputeJSON({
11 | prompt: "Give me an African capital city and its approximate population.",
12 | json_schema: {
13 | type: "object",
14 | properties: {
15 | cityName: { type: "string" },
16 | country: { type: "string" },
17 | approximatePopulation: { type: "number", greaterThan: 100000 },
18 | },
19 | required: ["text"],
20 | },
21 | });
22 |
23 | const b = new ComputeText({
24 | prompt: sb.concat(
25 | "give me the leader of the country: ",
26 | sb.jq<"string">(a.future.json_object, ".country"),
27 | ),
28 | });
29 |
30 | const res = await substrate.run(a, b);
31 | console.log({ a: res.get(a), b: res.get(b) });
32 | }
33 | main();
34 |
--------------------------------------------------------------------------------
/examples/box.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, ComputeText, Box, sb } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const languages = ["swedish", "ukranian", "thai", "turkish"] as const;
11 |
12 | const texts: Record = languages.reduce(
13 | (nodes, language) => {
14 | return {
15 | ...nodes,
16 | [language]: new ComputeText({
17 | prompt: sb.interpolate`count to 10 in ${language}`,
18 | max_tokens: 50,
19 | }),
20 | };
21 | },
22 | {},
23 | );
24 |
25 | const box = new Box({
26 | value: languages.reduce((obj, language) => {
27 | return {
28 | ...obj,
29 | [language]: texts[language]!.future.text,
30 | };
31 | }, {}),
32 | });
33 |
34 | const res = await substrate.run(box);
35 |
36 | console.log({ box: res.get(box) });
37 | }
38 | main();
39 |
--------------------------------------------------------------------------------
/.github/workflows/node.js.yml:
--------------------------------------------------------------------------------
1 | # This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node
2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs
3 |
4 | name: Node.js CI
5 |
6 | on:
7 | push:
8 | branches: ["main"]
9 | pull_request:
10 | branches: ["main"]
11 |
12 | jobs:
13 | build:
14 | runs-on: ubuntu-latest
15 |
16 | strategy:
17 | matrix:
18 | node-version:
19 | - "16.18.1"
20 | - "18.17.1"
21 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
22 |
23 | steps:
24 | - uses: actions/checkout@v3
25 | - name: Use Node.js ${{ matrix.node-version }}
26 | uses: actions/setup-node@v3
27 | with:
28 | node-version: ${{ matrix.node-version }}
29 | cache: "npm"
30 | - run: npm ci
31 | - run: make format-check
32 | - run: make build # also ensures typechecking passes
33 | - run: make test
34 |
--------------------------------------------------------------------------------
/examples/streaming/node-http-basic/server.js:
--------------------------------------------------------------------------------
1 | import http from "node:http";
2 | import { Readable } from "node:stream";
3 | import querystring from "node:querystring";
4 | import { Substrate, Llama3Instruct70B } from "substrate";
5 |
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 | const PORT = 3000;
8 |
9 | // Create the server
10 | const server = http.createServer(async (req, res) => {
11 | try {
12 | const params = querystring.parse(req.url.split("?")[1] || "");
13 |
14 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
15 |
16 | const node = new Llama3Instruct70B({ prompt: params.prompt });
17 |
18 | const stream = await substrate.stream(node);
19 |
20 | res.writeHead(200, { "Content-Type": "text/event-stream" });
21 | Readable.from(stream.apiResponse.body).pipe(res);
22 | } catch (err) {
23 | console.error(err);
24 | res.writeHead(500, { "Content-Type": "text/plain" });
25 | res.end(err.message);
26 | }
27 | });
28 |
29 | // Start the server
30 | server.listen(PORT, () => {
31 | console.log(`Server is running at http://localhost:${PORT}`);
32 | });
33 |
--------------------------------------------------------------------------------
/LICENSE.txt:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Substrate
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/examples/image-generation.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, ComputeText, GenerateImage } from "substrate";
4 |
5 | async function main() {
6 | const substrate = new Substrate({ apiKey: process.env["SUBSTRATE_API_KEY"] });
7 |
8 | const scene = new ComputeText({
9 | prompt:
10 | "describe a highly detailed forest scene with something surprising happening in one sentence, be concise, like Hemingway would write it.",
11 | });
12 |
13 | const styles = [
14 | "whimsical post-impressionist watercolor",
15 | "1960's saturday cartoon",
16 | "woodblock printed",
17 | "art nouveau poster",
18 | "low-res 8-bit video game graphics",
19 | ];
20 |
21 | const images = styles.map((style) => {
22 | return new GenerateImage({
23 | prompt: scene.future.text.concat(` render in a ((${style})) style`),
24 | store: "hosted",
25 | });
26 | });
27 |
28 | const res = await substrate.run(scene, ...images);
29 |
30 | console.log({
31 | scene: res.get(scene),
32 | images: images.map((node, i: number) => ({
33 | style: styles[i],
34 | image: res.get(node).image_uri,
35 | })),
36 | });
37 | }
38 | main();
39 |
--------------------------------------------------------------------------------
/examples/json.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, ComputeJSON, ComputeText, sb } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const author = new ComputeJSON({
11 | prompt: "Who wrote Don Quixote?",
12 | json_schema: {
13 | type: "object",
14 | properties: {
15 | name: {
16 | type: "string",
17 | description: "The name of the author.",
18 | },
19 | bio: {
20 | type: "string",
21 | description: "Concise biography of the author.",
22 | },
23 | },
24 | },
25 | temperature: 0.4,
26 | max_tokens: 800,
27 | });
28 |
29 | const name = author.future.json_object.get("name");
30 | const bio = author.future.json_object.get("bio");
31 |
32 | const report = new ComputeText({
33 | prompt: sb.interpolate`Write a short summary about ${name} and make sure to use the following bio: ${bio}`,
34 | });
35 |
36 | const res = await substrate.run(author, report);
37 | console.log(JSON.stringify(res.json, null, 2));
38 | }
39 | main();
40 |
--------------------------------------------------------------------------------
/src/SubstrateResponse.ts:
--------------------------------------------------------------------------------
1 | import { AnyNode, NodeOutput } from "substrate/Nodes";
2 | import { NodeError } from "substrate/Error";
3 |
4 | /**
5 | * Response to a run request.
6 | */
7 | export class SubstrateResponse {
8 | public apiRequest: Request;
9 | public apiResponse: Response;
10 | public json: any;
11 |
12 | constructor(request: Request, response: Response, json: any = null) {
13 | this.apiRequest = request;
14 | this.apiResponse = response;
15 | this.json = json;
16 | }
17 |
18 | get requestId() {
19 | return this.apiRequest.headers.get("x-substrate-request-id");
20 | }
21 |
22 | /**
23 | * Returns an error from the `Node` if there was one.
24 | */
25 | getError(node: T): NodeError | undefined {
26 | // @ts-expect-error
27 | return node.output() instanceof NodeError ? node.output() : undefined;
28 | }
29 |
30 | /**
31 | * Returns the result for given `Node`.
32 | *
33 | * @throws {NodeError} when there was an error running the node.
34 | */
35 | get(node: T): NodeOutput {
36 | const err = this.getError(node);
37 | if (err) throw err;
38 | // @ts-expect-error
39 | return node.output() as NodeOutput;
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/examples/mixture-of-agents/util.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from "url";
2 | import { dirname } from "path";
3 |
4 | export const sampleQuestion =
5 | "The following is a hypothetical short story written by Asimov after seeing the world in 2024. Go beyond the obvious, and come up with a creative story that is incisive, allegorical, and relevant. Respond starting with the title on the first line, followed by two newlines, and then the story.";
6 | export const aggregate = `You have been provided with a set of responses to the a user query. Your task is to synthesize these responses into a single, high-quality response. It is crucial to critically evaluate the information provided in these responses, recognizing that some of it may be biased or incorrect. Your response should not simply replicate the given answers but should offer a refined, accurate, and comprehensive reply to the original user query. Ensure your response is well-structured, well-considered, and adheres to the highest standards of accuracy and reliability. Do not respond conversationally or acknowledge the asking of the query, just output an objective response.`;
7 | export const jqList = `to_entries | map(((.key + 1) | tostring) + ". " + .value) | join("\n")`;
8 |
9 | // @ts-ignore
10 | export const currentDir = dirname(fileURLToPath(import.meta.url));
11 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "substrate",
3 | "version": "120240617.1.9",
4 | "description": "The official SDK for the Substrate API",
5 | "repository": {
6 | "type": "git",
7 | "url": "https://github.com/SubstrateLabs/substrate-typescript.git"
8 | },
9 | "keywords": [],
10 | "type": "module",
11 | "engines": {
12 | "node": ">=16"
13 | },
14 | "files": [
15 | "dist/**/*",
16 | "src/**/*",
17 | "!**/*.py"
18 | ],
19 | "scripts": {},
20 | "author": {
21 | "name": "Substrate Team",
22 | "email": "support@substrate.com"
23 | },
24 | "license": "MIT",
25 | "types": "./dist/index.d.ts",
26 | "exports": {
27 | "node": {
28 | "types": "./dist/nodejs/index.d.ts",
29 | "import": "./dist/nodejs/index.js",
30 | "require": "./dist/nodejs/index.cjs"
31 | },
32 | "types": "./dist/index.d.ts",
33 | "import": "./dist/index.js",
34 | "require": "./dist/index.cjs"
35 | },
36 | "devDependencies": {
37 | "@types/pako": "^2.0.3",
38 | "prettier": "3.3.3",
39 | "ts-node": "^10.9.2",
40 | "tsup": "^8.0.1",
41 | "typescript": "^5.3.3",
42 | "vite-tsconfig-paths": "^4.2.2",
43 | "vitest": "^1.0.4"
44 | },
45 | "dependencies": {
46 | "@types/node-fetch": "^2.6.11",
47 | "node-fetch": "2.7.0",
48 | "pako": "^2.1.0"
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/bin/update-version.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { readFileSync, writeFileSync } from "fs";
4 | import { execSync } from "node:child_process";
5 |
6 | // NOTE: Merged with API version to produce the full SDK version string
7 | // https://docs.substrate.run/versioning
8 | const SDK_VERSION = "1.1.9";
9 |
10 | const ok = (message: string) => console.log("\x1b[32m✓\x1b[0m", message);
11 |
12 | try {
13 | const version = readFileSync("src/GEN_VERSION", "utf-8").split(".")[0];
14 | const [major, minor, patch] = SDK_VERSION.split(".");
15 | const newVersion = `${major}${version}.${minor}.${patch}`;
16 | const packageJsonPath = "package.json";
17 | const packageJson = JSON.parse(readFileSync(packageJsonPath, "utf-8"));
18 | packageJson.version = newVersion;
19 | writeFileSync(packageJsonPath, JSON.stringify(packageJson, null, 2) + "\n");
20 | ok(`Updated package.json to version ${newVersion}`);
21 | execSync(`npm install`); // updates package-lock.json
22 | ok(`Updated package-lock.json`);
23 | const versionTsPath = "src/version.ts";
24 | const versionExport = `export const VERSION = "${newVersion}";\n`;
25 | writeFileSync(versionTsPath, versionExport);
26 | ok(`Updated version.ts to version ${newVersion}`);
27 | } catch (error) {
28 | console.error("Error reading or parsing openapi.json:", error);
29 | }
30 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * 𐃏 Substrate TypeScript SDK
3 | * @generated file
4 | * 20240617.20240815
5 | */
6 |
7 | export { SubstrateError } from "substrate/Error";
8 | export {
9 | Experimental,
10 | Box,
11 | If,
12 | ComputeText,
13 | MultiComputeText,
14 | BatchComputeText,
15 | BatchComputeJSON,
16 | ComputeJSON,
17 | MultiComputeJSON,
18 | GenerateCode,
19 | MultiGenerateCode,
20 | Mistral7BInstruct,
21 | Mixtral8x7BInstruct,
22 | Llama3Instruct8B,
23 | Llama3Instruct70B,
24 | Firellava13B,
25 | GenerateImage,
26 | MultiGenerateImage,
27 | InpaintImage,
28 | MultiInpaintImage,
29 | StableDiffusionXLLightning,
30 | StableDiffusionXLInpaint,
31 | StableDiffusionXLControlNet,
32 | StableVideoDiffusion,
33 | InterpolateFrames,
34 | TranscribeSpeech,
35 | GenerateSpeech,
36 | RemoveBackground,
37 | EraseImage,
38 | UpscaleImage,
39 | SegmentUnderPoint,
40 | SegmentAnything,
41 | SplitDocument,
42 | EmbedText,
43 | MultiEmbedText,
44 | EmbedImage,
45 | MultiEmbedImage,
46 | JinaV2,
47 | CLIP,
48 | FindOrCreateVectorStore,
49 | ListVectorStores,
50 | DeleteVectorStore,
51 | QueryVectorStore,
52 | FetchVectors,
53 | UpdateVectors,
54 | DeleteVectors,
55 | } from "substrate/Nodes";
56 |
57 | export { sb } from "substrate/sb";
58 | export { Substrate };
59 | import { Substrate } from "substrate/Substrate";
60 | export default Substrate;
61 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-basic/app/Demo.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { sb } from "substrate";
5 |
6 | export default function Demo() {
7 | const [output, setOutput] = useState("");
8 |
9 | async function submitPrompt(event: any) {
10 | event.preventDefault();
11 |
12 | const request = new Request("/api/generate-text", {
13 | method: "POST",
14 | body: new FormData(event.currentTarget),
15 | });
16 | const response = await fetch(request);
17 |
18 | if (response.ok) {
19 | setOutput("");
20 | const stream = await sb.streaming.fromSSEResponse(response);
21 |
22 | for await (let message of stream) {
23 | if (message.object === "node.delta") {
24 | setOutput((state) => state + message.data.choices.item.text);
25 | }
26 | }
27 | }
28 | }
29 |
30 | return (
31 |
32 |
44 |
45 |
{output}
46 |
47 | );
48 | }
49 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PACKAGE_VERSION=$(shell node -p -e "require('./package.json').version")
2 |
3 | # override this when publishing with an alternative distribution, eg. rc | experimental | demo
4 | NPM_TAG=latest
5 |
6 | node_modules/:
7 | npm install
8 |
9 | .PHONY: ensure
10 | ensure: node_modules/
11 |
12 | .PHONY: test
13 | test: ensure
14 | npx vitest --run
15 |
16 | .PHONY: test-watch
17 | test-watch: ensure
18 | npx vitest
19 |
20 | .PHONY: update-version
21 | update-version: ensure
22 | bin/update-version.ts
23 |
24 | .PHONY: sync-codegen
25 | sync-codegen: ensure
26 | bin/sync-codegen.ts
27 | make update-version
28 | make format-fix
29 |
30 | .PHONY: format-check
31 | format-check: ensure
32 | npx prettier . --check
33 |
34 | .PHONY: format-fix
35 | format-fix: ensure
36 | npx prettier . --write
37 |
38 | .PHONY: build
39 | build: ensure
40 | npx tsup
41 |
42 | .PHONY: build-watch
43 | build-watch: ensure
44 | npx tsup --watch
45 |
46 | .PHONY: publish-preview
47 | publish-preview: test build
48 | npm publish --tag=${NPM_TAG} --dry-run
49 |
50 | .PHONY: verify-publish
51 | verify-publish:
52 | @echo
53 | @echo "🌀 Publishing to NPM"
54 | @echo "Version: ${PACKAGE_VERSION}"
55 | @echo "Distribution Tag: ${NPM_TAG}"
56 | @echo "This will be live. Continue? [y/N] " && read ans && [ $${ans:-N} == y ]
57 |
58 | .PHONY: publish
59 | publish: verify-publish test build
60 | npm login
61 | npm publish --tag=${NPM_TAG}
62 | git tag "v${PACKAGE_VERSION}" && git push --tags
63 |
--------------------------------------------------------------------------------
/examples/knowledge-graph/util.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from "url";
2 | import { dirname } from "path";
3 |
4 | export const edgeColors = [
5 | "Charcoal Gray",
6 | "Deep Olive",
7 | "Navy Blue",
8 | "Burgundy",
9 | "Forest Green",
10 | "Dark Taupe",
11 | "Slate Blue",
12 | "Deep Plum",
13 | "Moss Green",
14 | "Dark Sienna",
15 | ];
16 | export const colors = [
17 | "Soft Sage",
18 | "Dusty Blue",
19 | "Pale Peach",
20 | "Muted Lavender",
21 | "Light Teal",
22 | "Warm Sand",
23 | "Faded Denim",
24 | "Dusty Rose",
25 | "Soft Mint",
26 | "Muted Coral",
27 | ];
28 | export const jsonSchema = {
29 | type: "object",
30 | properties: {
31 | nodes: {
32 | type: "array",
33 | items: {
34 | type: "object",
35 | properties: {
36 | id: { type: "integer" },
37 | label: { type: "string" },
38 | color: { type: "string", enum: colors },
39 | },
40 | },
41 | },
42 | edges: {
43 | type: "array",
44 | items: {
45 | type: "object",
46 | properties: {
47 | source: { type: "integer" },
48 | target: { type: "integer" },
49 | label: { type: "string" },
50 | color: { type: "string", enum: edgeColors },
51 | },
52 | },
53 | },
54 | },
55 | required: ["nodes", "edges"],
56 | description: "A knowledge graph with nodes and edges.",
57 | };
58 |
59 | // @ts-ignore
60 | export const currentDir = dirname(fileURLToPath(import.meta.url));
61 |
--------------------------------------------------------------------------------
/examples/knowledge-graph/generate.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, ComputeJSON, ComputeText, sb } from "substrate";
4 | import fs from "fs";
5 | import { currentDir, jsonSchema } from "./util";
6 | const sampleBook = "War and Peace by Tolstoy";
7 | const book = process.argv[2] || sampleBook;
8 | const substrate = new Substrate({ apiKey: process.env["SUBSTRATE_API_KEY"] });
9 |
10 | const opts = { cache_age: 60 * 60 * 24 * 7 };
11 | async function main() {
12 | const initialList = new ComputeText(
13 | {
14 | prompt: `List all the main characters in "${book}", then for all the meaningful relationships between them, provide a short label for the relationship`,
15 | model: "Llama3Instruct405B",
16 | },
17 | opts,
18 | );
19 | const graph = new ComputeJSON(
20 | {
21 | prompt: sb.interpolate`Make a JSON graph composed of the characters in ${book} that illustrates the relationships between them.
22 | Use this context:
23 | ${initialList.future.text}`,
24 | json_schema: jsonSchema,
25 | temperature: 0.2,
26 | model: "Llama3Instruct8B",
27 | },
28 | opts,
29 | );
30 | const res = await substrate.run(graph);
31 | const jsonOut = res.get(graph).json_object;
32 | const htmlTemplate = fs.readFileSync(`${currentDir}/index.html`, "utf8");
33 | const html = htmlTemplate
34 | .replace('"{{ graphData }}"', JSON.stringify(jsonOut, null, 2))
35 | .replaceAll("{{ title }}", book);
36 | fs.writeFileSync("knowledge-graph.html", html);
37 | }
38 | main();
39 |
--------------------------------------------------------------------------------
/src/Streaming.ts:
--------------------------------------------------------------------------------
1 | /** Represents an array item within a `Node` output chunk, specifies the field is an array containing this `item` at the `index`. **/
2 | type ChunkArrayItem = {
3 | object: "array.item";
4 | index: number;
5 | item: T;
6 | };
7 |
8 | /** Helper types for producing the "Chunk" types used in the `NodeDelta` messages */
9 | type ChunkizeObject = T extends object
10 | ? { [P in keyof T]: ChunkizeAny }
11 | : T;
12 |
13 | type ChunkizeArray = T extends (infer U)[]
14 | ? ChunkArrayItem>
15 | : ChunkArrayItem;
16 |
17 | type ChunkizeAny = T extends (infer U)[]
18 | ? ChunkizeArray
19 | : T extends object
20 | ? ChunkizeObject
21 | : T;
22 |
23 | /** Stream message that contains the completed `Node` output */
24 | type NodeResult = {
25 | object: "node.result";
26 | nodeId: string;
27 | data: T;
28 | };
29 |
30 | /** Stream message that contains a chunk of the `Node` output */
31 | type NodeDelta = {
32 | object: "node.delta";
33 | nodeId: string;
34 | data: ChunkizeAny;
35 | };
36 |
37 | /** Stream message when an error happened during a `Node` run. */
38 | export type NodeError = {
39 | object: "node.error";
40 | nodeId: string;
41 | data: {
42 | type: string;
43 | message: string;
44 | };
45 | };
46 |
47 | /** Stream message that contains the completed "Graph" output */
48 | export type GraphResult = {
49 | object: "graph.result";
50 | data: T;
51 | };
52 |
53 | export type NodeMessage = NodeResult | NodeDelta | NodeError;
54 |
55 | export type SSEMessage = NodeMessage | GraphResult;
56 |
--------------------------------------------------------------------------------
/examples/descript/generate.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 | import fs from "fs";
3 | import { ComputeText, sb, Substrate, TranscribeSpeech } from "substrate";
4 | import { currentDir } from "./util";
5 |
6 | /**
7 | * Other hosted audio files:
8 | * https://media.substrate.run/federer-dartmouth.m4a
9 | * https://media.substrate.run/kaufman-bafta-short.mp3
10 | * https://media.substrate.run/dfw-clip.m4a
11 | */
12 | // const sample = "https://media.substrate.run/my-dinner-andre.m4a"; // NB: this is a ~2hr long file
13 | const sample = "https://media.substrate.run/federer-dartmouth.m4a";
14 | const substrate = new Substrate({ apiKey: process.env["SUBSTRATE_API_KEY"] });
15 |
16 | const audio_uri = process.argv[2] || sample;
17 | const outfile = process.argv[3] || "descript.html";
18 | async function main() {
19 | const transcribe = new TranscribeSpeech(
20 | { audio_uri, segment: true, align: true },
21 | { cache_age: 60 * 60 * 24 * 7 },
22 | );
23 | // const summarize = new ComputeText({
24 | // model: "Llama3Instruct70B",
25 | // prompt: sb.interpolate`summarize this transcript: ${transcribe.future.text}`,
26 | // max_tokens: 800,
27 | // });
28 |
29 | const res = await substrate.run(transcribe);
30 | const transcript = res.get(transcribe);
31 |
32 | const htmlTemplate = fs.readFileSync(`${currentDir}/index.html`, "utf8");
33 | const html = htmlTemplate
34 | .replace('"{{ segments }}"', JSON.stringify(transcript.segments, null, 2))
35 | .replace("{{ audioUrl }}", audio_uri);
36 | fs.writeFileSync(outfile, html);
37 | }
38 |
39 | main().then(() => console.log(`꩜ Done. View by running \`open ${outfile}\``));
40 |
--------------------------------------------------------------------------------
/tests/Node.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, describe, test } from "vitest";
2 | import { Node } from "substrate/Node";
3 | import { FutureString, Trace, StringConcat } from "substrate/Future";
4 |
5 | class FooNode extends Node {}
6 |
7 | describe("Node", () => {
8 | test(".node", () => {
9 | const n = new FooNode({});
10 | expect(n.node).toEqual("FooNode");
11 | expect(n.id.startsWith("FooNode"));
12 | expect(n.id.includes("_"));
13 | });
14 |
15 | test(".toJSON", () => {
16 | const a = new FutureString(new Trace([], new FooNode({})));
17 | const b = new FutureString(new Trace([], new FooNode({})));
18 | const c = new FutureString(new StringConcat([a, b]));
19 | const d = new FutureString(new StringConcat([c, "d"]));
20 | const n = new FooNode({ prompt: d });
21 |
22 | expect(n.toJSON()).toEqual({
23 | id: n.id,
24 | node: "FooNode",
25 | _should_output_globally: true,
26 | args: {
27 | // @ts-expect-error (accessing protected property)
28 | prompt: d.toPlaceholder(),
29 | },
30 | });
31 | });
32 |
33 | test(".references", () => {
34 | const a = new FooNode({ x: "x" }, { id: "a" });
35 | const f1 = a.future.get("x");
36 | const f2 = a.future.get("y");
37 | const b = new FooNode({ x: f1, z: f2 }, { id: "b" });
38 | const f3 = b.future.get("x");
39 | const c = new FooNode({ x: f3 }, { id: "c" });
40 | const d = new FooNode({}, { id: "d", depends: [c] });
41 |
42 | // @ts-ignore (protected)
43 | const { nodes, futures } = d.references();
44 |
45 | expect(nodes).toEqual(new Set([a, b, c, d]));
46 | expect(futures).toEqual(new Set([f1, f2, f3]));
47 | });
48 | });
49 |
--------------------------------------------------------------------------------
/examples/if.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, ComputeJSON, Box, If, sb } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const planets = ["Jupiter", "Mars"];
11 |
12 | const sizes = planets.map((planet) => {
13 | return new ComputeJSON({
14 | prompt: sb.interpolate`How big is ${planet}?`,
15 | json_schema: {
16 | type: "object",
17 | properties: {
18 | planetName: {
19 | type: "string",
20 | description: "The name of the planet",
21 | enum: planets,
22 | },
23 | radius: {
24 | type: "string",
25 | description: "The radius of the planet in kilometers",
26 | },
27 | },
28 | },
29 | });
30 | });
31 |
32 | const [jupiter, mars] = sizes as [ComputeJSON, ComputeJSON];
33 | const radius = (p: ComputeJSON) =>
34 | p.future.json_object.get("radius") as unknown as number;
35 |
36 | const comparison = new ComputeJSON({
37 | prompt: sb.interpolate`Is ${radius(jupiter)} > ${radius(mars)}?`,
38 | json_schema: {
39 | type: "object",
40 | properties: {
41 | isGreaterThan: {
42 | type: "boolean",
43 | },
44 | },
45 | },
46 | });
47 |
48 | const result = new If({
49 | condition: comparison.future.json_object.get("isGreaterThan") as any,
50 | value_if_true: jupiter.future.json_object,
51 | value_if_false: mars.future.json_object,
52 | });
53 |
54 | const output = new Box({
55 | value: sb.interpolate`The bigger planet is ${result.future.result.get(
56 | "planetName",
57 | )}!`,
58 | });
59 |
60 | const res = await substrate.run(output);
61 | console.log(res.get(output));
62 | }
63 | main();
64 |
--------------------------------------------------------------------------------
/examples/explicit-edges.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, Box } from "substrate";
4 |
5 | async function main() {
6 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
7 |
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | // One way to see that the edges determine the node run order is to use the RunPython node to print the
11 | // timestamp of when the node was run. Because the RunPython node isn't available in the TypeScript SDK
12 | // I've taken a pickled python function that does a `print(time.time())` and am overriding the nodes
13 | // here to be RunPython nodes instead.
14 | const toRunPython = (node: Box) => {
15 | node.node = "RunPython";
16 | node.args = {
17 | pkl_function:
18 | "gAWV5QEAAAAAAACMF2Nsb3VkcGlja2xlLmNsb3VkcGlja2xllIwOX21ha2VfZnVuY3Rpb26Uk5QoaACMDV9idWlsdGluX3R5cGWUk5SMCENvZGVUeXBllIWUUpQoSwBLAEsASwFLAktDQxBkAWQAbAB9AHwAoAChAFMAlE5LAIaUjAR0aW1llIWUaAuMOC9Vc2Vycy9saWFtL3dvcmsvc3Vic3RyYXRlLXB5dGhvbi9leGFtcGxlcy9ydW5fcHl0aG9uLnB5lIwKcHJpbnRfdGltZZRLKEMECAEIAZQpKXSUUpR9lCiMC19fcGFja2FnZV9flE6MCF9fbmFtZV9flIwIX19tYWluX1+UjAhfX2ZpbGVfX5RoDHVOTk50lFKUaACMEl9mdW5jdGlvbl9zZXRzdGF0ZZSTlGgXfZR9lChoE2gNjAxfX3F1YWxuYW1lX1+UaA2MD19fYW5ub3RhdGlvbnNfX5R9lIwOX19rd2RlZmF1bHRzX1+UTowMX19kZWZhdWx0c19flE6MCl9fbW9kdWxlX1+UjAhfX21haW5fX5SMB19fZG9jX1+UTowLX19jbG9zdXJlX1+UTowXX2Nsb3VkcGlja2xlX3N1Ym1vZHVsZXOUXZSMC19fZ2xvYmFsc19flH2UdYaUhlIwLg==",
19 | kwargs: {},
20 | pip_install: null,
21 | python_version: "3.10.13",
22 | };
23 | return node;
24 | };
25 |
26 | const a = toRunPython(new Box({ value: "" }, { id: "a" }));
27 |
28 | const b = toRunPython(new Box({ value: "" }, { id: "b", depends: [a] }));
29 |
30 | const c = toRunPython(new Box({ value: "" }, { id: "c", depends: [b] }));
31 |
32 | const res = await substrate.run(c, a, b);
33 | console.log(res.json);
34 | }
35 | main();
36 |
--------------------------------------------------------------------------------
/examples/vector-store.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import {
4 | Substrate,
5 | QueryVectorStore,
6 | ListVectorStores,
7 | JinaV2,
8 | FindOrCreateVectorStore,
9 | DeleteVectorStore,
10 | FetchVectors,
11 | UpdateVectors,
12 | } from "substrate";
13 |
14 | async function main() {
15 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
16 |
17 | const substrate = new Substrate({
18 | apiKey: SUBSTRATE_API_KEY,
19 | baseUrl: "https://api-staging.substrate.run",
20 | });
21 |
22 | const create = new FindOrCreateVectorStore({
23 | collection_name: "vibes",
24 | model: "jina-v2",
25 | });
26 |
27 | const list = new ListVectorStores({});
28 |
29 | const insert = new JinaV2({
30 | items: [{ text: "tell me about celsius oasis vibe", doc_id: "celsius" }],
31 | collection_name: "vibes",
32 | });
33 |
34 | const query = new QueryVectorStore({
35 | collection_name: "vibes",
36 | model: "jina-v2",
37 | query_strings: ["celsius", "oasis vibe"],
38 | });
39 |
40 | const fetch = new FetchVectors({
41 | collection_name: "vibes",
42 | model: "jina-v2",
43 | ids: ["celsius"],
44 | });
45 |
46 | const update = new UpdateVectors({
47 | collection_name: "vibes",
48 | model: "jina-v2",
49 | vectors: [
50 | {
51 | id: "celsius",
52 | metadata: { some_metadata: "12345" },
53 | },
54 | ],
55 | });
56 |
57 | const destroy = new DeleteVectorStore({
58 | collection_name: "vibes",
59 | model: "jina-v2",
60 | });
61 |
62 | const res = await substrate.run(
63 | create,
64 | list,
65 | insert,
66 | query,
67 | fetch,
68 | update,
69 | destroy,
70 | );
71 |
72 | console.log("create", res.get(create));
73 | console.log("list", res.get(list));
74 | console.log("insert", res.get(insert));
75 | console.log("query", res.get(query));
76 | console.log("fetch", res.get(fetch));
77 | console.log("update", res.get(update));
78 | console.log("destroy", res.get(destroy));
79 | }
80 | main();
81 |
--------------------------------------------------------------------------------
/examples/descript/util.ts:
--------------------------------------------------------------------------------
1 | import { fileURLToPath } from "url";
2 | import { dirname } from "path";
3 |
4 | export const listChapters = `Please provide a list of sections for the following transcript.
5 | Such a list might be used to define a table of contents for a podcast or video.
6 | Section titles should be short, rarely more than 5 words.
7 | If it's a short transcript, you might only have 1 or 2 sections and if it's a long transcript, limit it to 16.
8 | These sections should be the main topics or themes of the transcript.
9 | They should be chronologically ordered.
10 | They should be roughly evenly spaced throughout the transcript.
11 | Do not neglect the latter end of the transcript
12 | Do not include minutiae.
13 | `;
14 |
15 | export const proposedSchema = {
16 | type: "object",
17 | properties: {
18 | chapters: {
19 | type: "array",
20 | items: { type: "string" },
21 | minItems: 1,
22 | maxItems: 16,
23 | },
24 | },
25 | };
26 |
27 | export const timestampPrompt = `You will be provided with a list of sections as well as a transcript.
28 | The sections will be used as a table of contents for that transcript.
29 | Based on the content of the transcript, please provide the approximate timestamp in seconds where each section begins.
30 |
31 | Lines are prefixed with the timestamp in seconds surrounded by square brackets. e.g.
32 | [59.73] Hello I'm going to talk about some topic.
33 |
34 | Your job is to analyze the semantics of the text and provide the timestamp (in seconds) where each section begins.
35 | The first timestamp should be 0.
36 | `;
37 |
38 | export const timestampedSchema = {
39 | type: "object",
40 | properties: {
41 | chapters: {
42 | type: "array",
43 | items: {
44 | type: "object",
45 | properties: {
46 | section: { type: "string" },
47 | start: { type: "number" },
48 | },
49 | required: ["section", "start"],
50 | },
51 | },
52 | },
53 | };
54 |
55 | // @ts-ignore
56 | export const currentDir = dirname(fileURLToPath(import.meta.url));
57 |
--------------------------------------------------------------------------------
/examples/descript/generate-chapters.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly --esm
2 | import fs from "fs";
3 | import { ComputeJSON, sb, Substrate, TranscribeSpeech } from "substrate";
4 | import {
5 | currentDir,
6 | listChapters,
7 | proposedSchema,
8 | timestampedSchema,
9 | timestampPrompt,
10 | } from "./util";
11 |
12 | /**
13 | * Other hosted audio files:
14 | * https://media.substrate.run/federer-dartmouth.m4a
15 | * https://media.substrate.run/kaufman-bafta-short.mp3
16 | * https://media.substrate.run/dfw-clip.m4a
17 | */
18 | const sample = "https://media.substrate.run/federer-dartmouth.m4a";
19 | const substrate = new Substrate({ apiKey: process.env["SUBSTRATE_API_KEY"] });
20 | const audio_uri = process.argv[2] || sample;
21 | const outfile = process.argv[3] || "descript.html";
22 | const opts = { cache_age: 60 * 60 * 24 * 7 };
23 | async function main() {
24 | const transcribe = new TranscribeSpeech(
25 | { audio_uri, segment: true, align: true },
26 | opts,
27 | );
28 | const chapters = new ComputeJSON(
29 | {
30 | prompt: sb.concat(
31 | listChapters,
32 | "\n\nTRANSCRIPT:\n\n",
33 | transcribe.future.text,
34 | ),
35 | json_schema: proposedSchema,
36 | model: "Mixtral8x7BInstruct",
37 | },
38 | opts,
39 | );
40 | const timestamps = new ComputeJSON(
41 | {
42 | prompt: sb.concat(
43 | timestampPrompt,
44 | "SECTIONS: ",
45 | sb.jq<"string">(chapters.future.json_object, ".chapters | @json"),
46 | "\n\nTRANSCRIPT:\n\n",
47 | transcribe.future.text,
48 | ),
49 | json_schema: timestampedSchema,
50 | model: "Mixtral8x7BInstruct",
51 | },
52 | opts,
53 | );
54 | const res = await substrate.run(transcribe, chapters, timestamps);
55 | const transcript = res.get(transcribe);
56 | // @ts-ignore
57 | const timestampedChapters = res.get(timestamps).json_object?.chapters;
58 |
59 | const htmlTemplate = fs.readFileSync(`${currentDir}/index.html`, "utf8");
60 | const html = htmlTemplate
61 | .replace('"{{ segments }}"', JSON.stringify(transcript.segments, null, 2))
62 | .replace('"{{ chapters }}"', JSON.stringify(timestampedChapters, null, 2))
63 | .replace("{{ audioUrl }}", audio_uri);
64 | fs.writeFileSync(outfile, html);
65 | }
66 |
67 | main().then(() => console.log(`꩜ Done. View by running \`open ${outfile}\``));
68 |
--------------------------------------------------------------------------------
/examples/mixture-of-agents/ask.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env -S npx ts-node --transpileOnly
2 |
3 | import { Substrate, Box, sb, ComputeText } from "substrate";
4 | import fs from "fs";
5 | import { currentDir, sampleQuestion, aggregate, jqList } from "./util";
6 |
7 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"];
8 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
9 |
10 | const models = [
11 | "Llama3Instruct405B",
12 | "claude-3-5-sonnet-20240620",
13 | "Llama3Instruct70B",
14 | "gpt-4o-mini",
15 | "Llama3Instruct8B",
16 | "Mixtral8x7BInstruct",
17 | ];
18 | const aggregatorModel = "claude-3-5-sonnet-20240620";
19 | const max_tokens = 400;
20 | const temperature = 0.4;
21 | const opts = { cache_age: 60 * 60 * 24 * 7 };
22 |
23 | const numLayers = 3;
24 | const question = process.argv[2] || sampleQuestion;
25 |
26 | function getPrompt(prev: any = null) {
27 | return prev
28 | ? sb.concat(
29 | aggregate,
30 | "\n\nuser query: ",
31 | question,
32 | "\n\nprevious responses:\n\n",
33 | prev,
34 | )
35 | : question;
36 | }
37 |
38 | function getMixture(prev: any = null) {
39 | return new Box({
40 | value: models.map(
41 | (model) =>
42 | new ComputeText(
43 | { prompt: getPrompt(prev), model, max_tokens, temperature },
44 | opts,
45 | ).future.text,
46 | ),
47 | });
48 | }
49 |
50 | function getLastLayer(layers: Box[]) {
51 | return sb.jq<"string">(layers[layers.length - 1]!.future.value, jqList);
52 | }
53 |
54 | async function main() {
55 | const layers: Box[] = [getMixture(question)];
56 | for (let i = 0; i < numLayers - 1; i++) {
57 | layers.push(getMixture(getLastLayer(layers)));
58 | }
59 | const final = new ComputeText(
60 | {
61 | prompt: getPrompt(getLastLayer(layers)),
62 | model: aggregatorModel,
63 | max_tokens: 800,
64 | temperature,
65 | },
66 | opts,
67 | );
68 | const box = new Box({
69 | value: {
70 | layers: layers.map((l) => l.future.value),
71 | final: final.future.text,
72 | },
73 | });
74 |
75 | const res = await substrate.run(box);
76 | const jsonOut: any = res.get(box).value;
77 |
78 | const htmlTemplate = fs.readFileSync(`${currentDir}/index.html`, "utf8");
79 | const html = htmlTemplate
80 | .replace('"{{ individual }}"', JSON.stringify(jsonOut.layers, null, 2))
81 | .replace('"{{ question }}"', JSON.stringify(question))
82 | .replace('"{{ summaries }}"', `[${JSON.stringify(jsonOut.final)}]`);
83 | fs.writeFileSync("moa.html", html);
84 | }
85 |
86 | main().then(() => console.log(`꩜ Done. View by running \`open moa.html\``));
87 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Substrate TypeScript SDK
2 |
3 | [](https://npmjs.org/package/substrate)
4 |
5 | Substrate is a **powerful SDK** for building with AI, with [batteries included](https://substrate.run/nodes): language models, image generation, built-in vector storage, sandboxed code execution, and more. To use Substrate, you simply connect tasks, and then run the workflow. With this simple approach, we can create AI systems (from RAG, to agents, to multi-modal generative experiences) by simply describing the computation, with **zero additional abstractions**.
6 |
7 | Substrate is also a **workflow execution** and **inference** engine, optimized for running compound AI workloads. Wiring together multiple inference APIs is inherently slow – whether you do it yourself, or use a framework like LangChain. Substrate lets you ditch the framework, write less code, and run compound AI fast.
8 |
9 | ## Documentation
10 |
11 | If you're just getting started, head to [docs.substrate.run](https://docs.substrate.run/).
12 |
13 | For a detailed API reference covering the nodes available on Substrate, see [substrate.run/nodes](https://www.substrate.run/nodes).
14 |
15 | ## Installation
16 |
17 | ```sh
18 | npm install substrate
19 | ```
20 |
21 | ## Usage
22 |
23 | ```typescript
24 | import { Substrate, ComputeText, sb } from "substrate";
25 | ```
26 |
27 | Initialize the Substrate client.
28 |
29 | ```typescript
30 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
31 | ```
32 |
33 | Generate a story using the [`ComputeText`](https://www.substrate.run/nodes#ComputeText) node.
34 |
35 | ```typescript
36 | const story = new ComputeText({ prompt: "tell me a story" });
37 | ```
38 |
39 | Summarize the output of the `story` node using another `ComputeText` node. Because `story` has not yet been run, we use `sb.interpolate` to work with its future output.
40 |
41 | ```typescript
42 | const summary = new ComputeText({
43 | prompt: sb.interpolate`summarize this story in one sentence: ${story.future.text}`,
44 | });
45 | ```
46 |
47 | Run the graph chaining `story` → `summary` by passing the terminal node to `substrate.run`.
48 |
49 | ```typescript
50 | const response = await substrate.run(summary);
51 | ```
52 |
53 | Get the output of the summary node by passing it to `response.get`.
54 |
55 | ```typescript
56 | const summaryOut = response.get(summary);
57 | console.log(summaryOut.text);
58 | // Princess Lily, a kind-hearted young princess, discovers a book of spells and uses it to grant her family and kingdom happiness.
59 | ```
60 |
61 | ## Examples
62 |
63 | We're always creating new JS examples on [val.town](https://www.val.town/u/substrate/folders/Examples?folderId=61e21628-4209-11ef-bf47-de64eea55b61).
64 |
65 | Many examples are also included in the `examples` directory.
66 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/app/Demo.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import { useState } from "react";
4 | import { sb } from "substrate";
5 |
6 | type Vote = {
7 | vote: string;
8 | confidence: string;
9 | commentary: string;
10 | };
11 |
12 | export default function Demo() {
13 | const [tally, setTally] = useState<{ [x: string]: number }>({});
14 | const [output, setOutput] = useState([]);
15 |
16 | async function submitPrompt(event: any) {
17 | event.preventDefault();
18 |
19 | const request = new Request("/api/this-or-that", {
20 | method: "POST",
21 | body: new FormData(event.currentTarget),
22 | });
23 | const response = await fetch(request);
24 |
25 | if (response.ok) {
26 | setOutput([]);
27 | setTally({});
28 |
29 | const stream = await sb.streaming.fromSSEResponse(response);
30 |
31 | for await (let message of stream) {
32 | // node.results are recieved when a node has completed it's run
33 | // it contains the entire node output in the `data` field of the
34 | // message
35 | if (message.object === "node.result") {
36 | if ("firstThing" in message.data.json_object) {
37 | setTally({
38 | [message.data.json_object.firstThing as string]: 0,
39 | [message.data.json_object.secondThing as string]: 0,
40 | });
41 | continue;
42 | }
43 |
44 | if ("vote" in message.data.json_object) {
45 | setOutput((state) => [...state, message.data.json_object]);
46 |
47 | setTally((state) => ({
48 | ...state,
49 | [message.data.json_object.vote as string]:
50 | state[message.data.json_object.vote] + 1,
51 | }));
52 | continue;
53 | }
54 | }
55 | }
56 | }
57 | }
58 |
59 | return (
60 |
61 |
73 |
74 |
75 | {Object.keys(tally).map((thing) => (
76 |
77 |
"{thing}"
78 |
{tally[thing]}
79 |
80 | ))}
81 |
82 |
83 |
84 | {output.map((item, i) => (
85 |
86 | +1 for "{item.vote}"
87 |
88 | Commentary 💬
89 | {item.commentary}
90 |
91 |
92 | ))}
93 |
94 |
95 | );
96 | }
97 |
--------------------------------------------------------------------------------
/examples/streaming/nextjs-multiple-nodes/app/api/this-or-that/route.ts:
--------------------------------------------------------------------------------
1 | "use server";
2 |
3 | import { z } from "zod";
4 | import zodToJsonSchema from "zod-to-json-schema";
5 | import { Substrate, ComputeJSON, sb } from "substrate";
6 |
7 | const SUBSTRATE_API_KEY = process.env["SUBSTRATE_API_KEY"]!;
8 |
9 | const substrate = new Substrate({ apiKey: SUBSTRATE_API_KEY });
10 |
11 | function extractThisAndThat(inputText: string) {
12 | const schema = z
13 | .object({
14 | firstThing: z.string().describe("The first thing being compared"),
15 | secondThing: z.string().describe("The second thing being compared"),
16 | })
17 | .describe("Two items that are being compared");
18 |
19 | return new ComputeJSON({
20 | prompt: `
21 | === Instructions
22 | Examine the following input text and extract the two things that are being compared.
23 |
24 | === Input Text
25 | ${inputText}`,
26 | json_schema: zodToJsonSchema(schema),
27 | temperature: 0.4,
28 | });
29 | }
30 |
31 | function voter(about: string, items: any) {
32 | const schema = z
33 | .object({
34 | vote: z.enum(items).describe("The item you vote for"),
35 | confidence: z
36 | .number()
37 | .min(0)
38 | .max(100)
39 | .describe("How confident you are in the vote"),
40 | commentary: z
41 | .string()
42 | .describe("Rationale for why you voted the way you did"),
43 | })
44 | .describe(
45 | "Your vote on which item you prefer and how strongly you feel about it",
46 | );
47 |
48 | return new ComputeJSON(
49 | {
50 | prompt: sb.interpolate`
51 | === About you
52 | ${about}
53 |
54 | === Your instructions
55 | Consider the following 2 items and based on your experience please vote for
56 | which one you prefer.
57 |
58 | Also include a confidence rating for how strongly you believe in your position
59 | and commentary on why you voted the way you did.
60 |
61 | You must vote for something, even if you have very no confidence.
62 |
63 | === Items to vote for
64 | * ${items[0]}
65 | * ${items[1]}
66 | `,
67 | json_schema: zodToJsonSchema(schema),
68 | },
69 | { id: about },
70 | );
71 | }
72 |
73 | export async function POST(request: Request) {
74 | const formData = await request.formData();
75 | const prompt = formData.get("prompt")!.toString();
76 |
77 | const thisAndThat = extractThisAndThat(prompt);
78 |
79 | const items = [
80 | thisAndThat.future.json_object.get("firstThing"),
81 | thisAndThat.future.json_object.get("secondThing"),
82 | ];
83 |
84 | const voters = [
85 | voter("You are a cattle rancher from Houston", items),
86 | voter("You are a semi-pro tennis player from Chicago", items),
87 | voter("You are computer programmer based in Los Angeles", items),
88 | voter("You are cab driver based in New York City", items),
89 | voter("You are college professor based out of Montreal", items),
90 | voter("You are an aspiring cellist that travels often to Vancouver", items),
91 | voter("You are nurse stationed outside Toronto", items),
92 | voter("You are a chef at a fine dining restuarant in Mexico City", items),
93 | voter("You are retired journalist that settled in Oaxaca", items),
94 | ];
95 |
96 | const streamResponse = await substrate.stream(thisAndThat, ...voters);
97 | const body = streamResponse.apiResponse.body;
98 |
99 | return new Response(body, {
100 | headers: { "Content-Type": "text/event-stream" },
101 | });
102 | }
103 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 |
5 | /* Language and Environment */
6 | "target": "es2020" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
7 |
8 | /* Modules */
9 | "module": "commonjs" /* Specify what module code is generated. */,
10 | "moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
11 | "resolveJsonModule": true,
12 | "esModuleInterop": true,
13 |
14 | /* Emit */
15 | "declaration": true /* Generate .d.ts files from TypeScript and JavaScript files in your project. */,
16 | "outDir": "./dist" /* Specify an output folder for all emitted files. */,
17 |
18 | /* Interop Constraints */
19 | "forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
20 |
21 | /* Type Checking */
22 | "strict": true /* Enable all strict type-checking options. */,
23 | "strictNullChecks": true /* When type checking, take into account 'null' and 'undefined'. */,
24 | "strictPropertyInitialization": false /* Check for class properties that are declared but not set in the constructor. */,
25 | "strictFunctionTypes": true /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */,
26 | "strictBindCallApply": true /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */,
27 | "alwaysStrict": true /* Ensure 'use strict' is always emitted. */,
28 | "noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied 'any' type. */,
29 | "noImplicitReturns": true /* Enable error reporting for codepaths that do not explicitly return in a function. */,
30 | "noUnusedLocals": true /* Enable error reporting when local variables aren't read. */,
31 | "noImplicitOverride": true /* Ensure overriding members in derived classes are marked with an override modifier. */,
32 | "noImplicitThis": true /* Enable error reporting when 'this' is given the type 'any'. */,
33 | "noUncheckedIndexedAccess": true /* Add 'undefined' to a type when accessed using an index. */,
34 | "noPropertyAccessFromIndexSignature": true /* Enforces using indexed accessors for keys declared using an indexed type. */,
35 | "noFallthroughCasesInSwitch": true /* Enable error reporting for fallthrough cases in switch statements. */,
36 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
37 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
38 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
39 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
40 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
41 |
42 | /* Completeness */
43 | "skipLibCheck": true /* Skip type checking all .d.ts files. */,
44 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
45 |
46 | /* Internal-use: package paths */
47 | "paths": {
48 | "substrate/*": ["./src/*"],
49 | "substrate": ["./src/index.ts"]
50 | }
51 | },
52 |
53 | /**
54 | * Internal-use: running example scripts
55 | * We're mostly developing on es2022/Node 18+, but to extend that support a bit futher
56 | * back our main compiler options are overridden when running with ts-node to use newer
57 | * syntax, eg. top-level await.
58 | */
59 | "ts-node": {
60 | "esm": true,
61 | "experimentalSpecifierResolution": "node",
62 | "compilerOptions": {
63 | "target": "es2022",
64 | "module": "es2022",
65 | "moduleResolution": "node",
66 | "esModuleInterop": true
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/SubstrateStreamingResponse.ts:
--------------------------------------------------------------------------------
1 | import { createParser } from "substrate/EventSource";
2 | import { NodeMessage, SSEMessage } from "substrate/Streaming";
3 | import { SubstrateError } from "substrate/Error";
4 | import { AnyNode, NodeOutput } from "substrate/Nodes";
5 |
6 | /**
7 | * `StreamingResponse` is an async iterator that is used to interact with a stream of Server-Sent Events
8 | */
9 | export class StreamingResponse {
10 | apiResponse: Response;
11 | iterator: any;
12 |
13 | constructor(response: Response, iterator: any) {
14 | this.apiResponse = response;
15 | this.iterator = iterator;
16 | }
17 |
18 | [Symbol.asyncIterator]() {
19 | return this.iterator;
20 | }
21 |
22 | tee(n: number = 2) {
23 | return tee(n, this.iterator).map(
24 | (iterator) => new StreamingResponse(this.apiResponse, iterator),
25 | );
26 | }
27 |
28 | static async fromReponse(response: Response) {
29 | if (!response.body) {
30 | throw new SubstrateError("Response body must be present");
31 | }
32 |
33 | const decoder = new TextDecoder("utf-8");
34 | const parser = createParser();
35 |
36 | async function* iterator(): AsyncGenerator {
37 | for await (const chunk of readableStreamAsyncIterable(response.body)) {
38 | for (const message of parser.getMessages(
39 | decoder.decode(chunk as any),
40 | )) {
41 | if (message.data) {
42 | try {
43 | const sseMessage = JSON.parse(message.data);
44 | yield sseMessage as SSEMessage;
45 | } catch (_err) {
46 | throw new SubstrateError(
47 | `Bad Server-Sent Event message: ${message}`,
48 | );
49 | }
50 | }
51 | }
52 | }
53 | }
54 |
55 | return new StreamingResponse(response, iterator());
56 | }
57 | }
58 |
59 | /**
60 | * `SubstrateStreamingResponse`
61 | */
62 | export class SubstrateStreamingResponse extends StreamingResponse {
63 | public apiRequest: Request;
64 |
65 | constructor(request: Request, response: Response, iterator: any) {
66 | super(response, iterator);
67 | this.apiRequest = request;
68 | }
69 |
70 | async *get(
71 | node: T,
72 | ): AsyncGenerator>> {
73 | for await (let message of this) {
74 | if (message?.node_id === node.id) {
75 | yield message as NodeMessage>;
76 | }
77 | }
78 | }
79 |
80 | override tee(n: number = 2) {
81 | return tee(n, this.iterator).map(
82 | (iterator) =>
83 | new SubstrateStreamingResponse(
84 | this.apiRequest,
85 | this.apiResponse,
86 | iterator,
87 | ),
88 | );
89 | }
90 |
91 | static async fromRequestReponse(request: Request, response: Response) {
92 | const streamingResponse = await StreamingResponse.fromReponse(response);
93 | return new SubstrateStreamingResponse(
94 | request,
95 | response,
96 | streamingResponse.iterator,
97 | );
98 | }
99 | }
100 |
101 | function readableStreamAsyncIterable(stream: any) {
102 | // When stream is already an iterator we return it. This is the case when using a
103 | // `response.body` from node-fetch.
104 | if (stream[Symbol.asyncIterator]) return stream;
105 |
106 | // Otherwise we use getReader and produce an async iterable from the ReadableStream.
107 | // This is the variant we would see when using an implementation of fetch closer to
108 | // the web.
109 | const reader = stream.getReader();
110 | return {
111 | async next() {
112 | try {
113 | const result = await reader.read();
114 | if (result?.done) reader.releaseLock(); // release lock when stream becomes closed
115 | return result;
116 | } catch (e) {
117 | reader.releaseLock(); // release lock when stream becomes errored
118 | throw e;
119 | }
120 | },
121 | async return() {
122 | const cancelPromise = reader.cancel();
123 | reader.releaseLock();
124 | await cancelPromise;
125 | return { done: true, value: undefined };
126 | },
127 | [Symbol.asyncIterator]() {
128 | return this;
129 | },
130 | };
131 | }
132 |
133 | function tee(n: number = 2, iterator: any) {
134 | const queues: any[] = [];
135 | for (let i = 0; i < n; i++) {
136 | queues.push([]);
137 | }
138 |
139 | const teeIterator = (queue: SSEMessage[]) => {
140 | return {
141 | next: () => {
142 | if (queue.length === 0) {
143 | const result = iterator.next();
144 | for (let q of queues) q.push(result);
145 | }
146 | return queue.shift();
147 | },
148 | };
149 | };
150 |
151 | return queues.map((q) => teeIterator(q));
152 | }
153 |
--------------------------------------------------------------------------------
/tests/SubstrateResponse.test.ts:
--------------------------------------------------------------------------------
1 | // including polyfill for node16 support
2 | import "substrate/nodejs/polyfill";
3 |
4 | import { expect, describe, test } from "vitest";
5 | import { SubstrateResponse } from "substrate/SubstrateResponse";
6 | import { Node } from "substrate/Node";
7 | import { NodeError } from "substrate/Error";
8 |
9 | class FooNode extends Node {}
10 |
11 | describe("SubstrateResponse", () => {
12 | test(".requestId null when not present in response", () => {
13 | // When there is no request-id present in the response headers.
14 | const request = new Request("http://127.0.0.1");
15 | const response = new Response();
16 | const responseJSON = {};
17 | const sbResponse = new SubstrateResponse(request, response, responseJSON);
18 |
19 | expect(sbResponse.requestId).toBeNull();
20 | });
21 |
22 | test(".requestId present when present in request", () => {
23 | // When there is no request-id present in the response headers.
24 | const request = new Request("http://127.0.0.1", {
25 | headers: { "x-substrate-request-id": "REQUEST_ID" },
26 | });
27 | const response = new Response();
28 | const responseJSON = {};
29 | const sbResponse = new SubstrateResponse(request, response, responseJSON);
30 |
31 | expect(sbResponse.requestId).toEqual("REQUEST_ID");
32 | });
33 |
34 | test(".getError returns a `NodeError` when node output in an error", () => {
35 | // NOTE: we're selecting the response off the Node currently to support a previous
36 | // design, but may change this internal implementation
37 | const node = new FooNode({}, { id: "nodeId" });
38 |
39 | const request = new Request("http://127.0.0.1");
40 | const response = new Response();
41 | const responseJSON = {
42 | data: {
43 | [node.id]: {
44 | type: "error_type",
45 | message: "error_message",
46 | request_id: "error_request_id",
47 | },
48 | },
49 | };
50 | const sbResponse = new SubstrateResponse(request, response, responseJSON);
51 | // @ts-expect-error (accessing protected)
52 | node.response = sbResponse;
53 |
54 | const nodeError = new NodeError(
55 | "error_type",
56 | "error_message",
57 | "error_request_id",
58 | );
59 |
60 | // @ts-expect-error: the type AnyNode here should be expanded to encompass all Node instances, but it isn't yet.
61 | expect(sbResponse.getError(node)).toEqual(nodeError);
62 | });
63 |
64 | test(".getError returns a `undefined` when node output is not an error", () => {
65 | // NOTE: we're selecting the response off the Node currently to support a previous
66 | // design, but may change this internal implementation
67 | const node = new FooNode({}, { id: "nodeId" });
68 |
69 | const request = new Request("http://127.0.0.1");
70 | const response = new Response();
71 | const responseJSON = {
72 | data: {
73 | [node.id]: {
74 | nodeOutput: "something_that_is_not_an_error",
75 | },
76 | },
77 | };
78 | const sbResponse = new SubstrateResponse(request, response, responseJSON);
79 | // @ts-expect-error (accessing protected)
80 | node.response = sbResponse;
81 |
82 | // @ts-expect-error: the type AnyNode here should be expanded to encompass all Node instances, but it isn't yet.
83 | expect(sbResponse.getError(node)).toBeUndefined();
84 | });
85 |
86 | test(".get throws `NodeError` when node output is an error", () => {
87 | // NOTE: we're selecting the response off the Node currently to support a previous
88 | // design, but may change this internal implementation
89 | const node = new FooNode({}, { id: "nodeId" });
90 |
91 | const request = new Request("http://127.0.0.1");
92 | const response = new Response();
93 | const responseJSON = {
94 | data: {
95 | [node.id]: {
96 | type: "error_type",
97 | message: "error_message",
98 | request_id: "error_request_id",
99 | },
100 | },
101 | };
102 | const sbResponse = new SubstrateResponse(request, response, responseJSON);
103 | // @ts-expect-error (accessing protected)
104 | node.response = sbResponse;
105 |
106 | // @ts-expect-error: the type AnyNode here should be expanded to encompass all Node instances, but it isn't yet.
107 | expect(() => sbResponse.get(node)).toThrowError(/error_message/);
108 | });
109 |
110 | test(".get returns node output when node output is not an error", () => {
111 | // NOTE: we're selecting the response off the Node currently to support a previous
112 | // design, but may change this internal implementation
113 | const node = new FooNode({}, { id: "nodeId" });
114 |
115 | const request = new Request("http://127.0.0.1");
116 | const response = new Response();
117 | const responseJSON = {
118 | data: {
119 | [node.id]: {
120 | nodeOutput: "something_that_is_not_an_error",
121 | },
122 | },
123 | };
124 | const sbResponse = new SubstrateResponse(request, response, responseJSON);
125 | // @ts-expect-error (accessing protected)
126 | node.response = sbResponse;
127 |
128 | // @ts-expect-error: the type AnyNode here should be expanded to encompass all Node instances, but it isn't yet.
129 | expect(sbResponse.get(node)).toEqual({
130 | nodeOutput: "something_that_is_not_an_error",
131 | });
132 | });
133 | });
134 |
--------------------------------------------------------------------------------
/src/Platform.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @private
3 | *
4 | * Returns properties of the current environment.
5 | */
6 |
7 | declare const Deno: any;
8 | declare const EdgeRuntime: any;
9 |
10 | type Arch = "x32" | "x64" | "arm" | "arm64" | `other:${string}` | "unknown";
11 |
12 | type PlatformName =
13 | | "MacOS"
14 | | "Linux"
15 | | "Windows"
16 | | "FreeBSD"
17 | | "OpenBSD"
18 | | "iOS"
19 | | "Android"
20 | | `Other:${string}`
21 | | "Unknown";
22 |
23 | type Browser = "ie" | "edge" | "chrome" | "firefox" | "safari";
24 |
25 | type PlatformProperties = {
26 | os: PlatformName;
27 | arch: Arch;
28 | runtime:
29 | | "node"
30 | | "deno"
31 | | "edge"
32 | | "workerd"
33 | | `browser:${Browser}`
34 | | "unknown";
35 | runtimeVersion: string;
36 | };
37 |
38 | export const getPlatformProperties = (): PlatformProperties => {
39 | if (typeof Deno !== "undefined" && Deno.build != null) {
40 | return {
41 | os: normalizePlatform(Deno.build.os),
42 | arch: normalizeArch(Deno.build.arch),
43 | runtime: "deno",
44 | runtimeVersion:
45 | typeof Deno.version === "string"
46 | ? Deno.version
47 | : (Deno.version?.deno ?? "unknown"),
48 | };
49 | }
50 | if (typeof EdgeRuntime !== "undefined") {
51 | return {
52 | os: "Unknown",
53 | arch: `other:${EdgeRuntime}`,
54 | runtime: "edge",
55 | runtimeVersion: process.version,
56 | };
57 | }
58 | // Check if Node.js
59 | if (
60 | Object.prototype.toString.call(
61 | typeof process !== "undefined" ? process : 0,
62 | ) === "[object process]"
63 | ) {
64 | return {
65 | os: normalizePlatform(process.platform),
66 | arch: normalizeArch(process.arch),
67 | runtime: "node",
68 | runtimeVersion: process.version,
69 | };
70 | }
71 |
72 | // https://developers.cloudflare.com/workers/runtime-apis/web-standards/#navigatoruseragent
73 | if (
74 | typeof navigator !== undefined &&
75 | navigator.userAgent === "Cloudflare-Workers"
76 | ) {
77 | return {
78 | os: "Unknown",
79 | arch: "unknown",
80 | runtime: "workerd",
81 | runtimeVersion: "unknown",
82 | };
83 | }
84 |
85 | const browserInfo = getBrowserInfo();
86 | if (browserInfo) {
87 | return {
88 | os: "Unknown",
89 | arch: "unknown",
90 | runtime: `browser:${browserInfo.browser}`,
91 | runtimeVersion: browserInfo.version,
92 | };
93 | }
94 |
95 | return {
96 | os: "Unknown",
97 | arch: "unknown",
98 | runtime: "unknown",
99 | runtimeVersion: "unknown",
100 | };
101 | };
102 |
103 | type BrowserInfo = {
104 | browser: Browser;
105 | version: string;
106 | };
107 |
108 | // Note: modified from https://github.com/JS-DevTools/host-environment/blob/b1ab79ecde37db5d6e163c050e54fe7d287d7c92/src/isomorphic.browser.ts
109 | function getBrowserInfo(): BrowserInfo | null {
110 | if (typeof navigator === "undefined" || !navigator) {
111 | return null;
112 | }
113 |
114 | // NOTE: The order matters here!
115 | const browserPatterns = [
116 | { key: "edge" as const, pattern: /Edge(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
117 | { key: "ie" as const, pattern: /MSIE(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/ },
118 | {
119 | key: "ie" as const,
120 | pattern: /Trident(?:.*rv\:(\d+)\.(\d+)(?:\.(\d+))?)?/,
121 | },
122 | {
123 | key: "chrome" as const,
124 | pattern: /Chrome(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/,
125 | },
126 | {
127 | key: "firefox" as const,
128 | pattern: /Firefox(?:\W+(\d+)\.(\d+)(?:\.(\d+))?)?/,
129 | },
130 | {
131 | key: "safari" as const,
132 | pattern:
133 | /(?:Version\W+(\d+)\.(\d+)(?:\.(\d+))?)?(?:\W+Mobile\S*)?\W+Safari/,
134 | },
135 | ];
136 |
137 | // Find the FIRST matching browser
138 | for (const { key, pattern } of browserPatterns) {
139 | const match = pattern.exec(navigator.userAgent);
140 | if (match) {
141 | const major = match[1] || 0;
142 | const minor = match[2] || 0;
143 | const patch = match[3] || 0;
144 |
145 | return { browser: key, version: `${major}.${minor}.${patch}` };
146 | }
147 | }
148 |
149 | return null;
150 | }
151 |
152 | const normalizeArch = (arch: string): Arch => {
153 | // Node docs:
154 | // - https://nodejs.org/api/process.html#processarch
155 | // Deno docs:
156 | // - https://doc.deno.land/deno/stable/~/Deno.build
157 | if (arch === "x32") return "x32";
158 | if (arch === "x86_64" || arch === "x64") return "x64";
159 | if (arch === "arm") return "arm";
160 | if (arch === "aarch64" || arch === "arm64") return "arm64";
161 | if (arch) return `other:${arch}`;
162 | return "unknown";
163 | };
164 |
165 | const normalizePlatform = (platform: string): PlatformName => {
166 | // Node platforms:
167 | // - https://nodejs.org/api/process.html#processplatform
168 | // Deno platforms:
169 | // - https://doc.deno.land/deno/stable/~/Deno.build
170 | // - https://github.com/denoland/deno/issues/14799
171 |
172 | platform = platform.toLowerCase();
173 |
174 | // NOTE: this iOS check is untested and may not work
175 | // Node does not work natively on IOS, there is a fork at
176 | // https://github.com/nodejs-mobile/nodejs-mobile
177 | // however it is unknown at the time of writing how to detect if it is running
178 | if (platform.includes("ios")) return "iOS";
179 | if (platform === "android") return "Android";
180 | if (platform === "darwin") return "MacOS";
181 | if (platform === "win32") return "Windows";
182 | if (platform === "freebsd") return "FreeBSD";
183 | if (platform === "openbsd") return "OpenBSD";
184 | if (platform === "linux") return "Linux";
185 | if (platform) return `Other:${platform}`;
186 | return "Unknown";
187 | };
188 |
--------------------------------------------------------------------------------
/tests/Substrate.test.ts:
--------------------------------------------------------------------------------
1 | import { expect, describe, test } from "vitest";
2 | import { Substrate } from "substrate/Substrate";
3 | import { Node } from "substrate/Node";
4 | import { sb } from "substrate/sb";
5 |
6 | class FooNode extends Node {}
7 |
8 | describe("Substrate", () => {
9 | describe(".serialize", () => {
10 | test("when there are nodes and futures", () => {
11 | const a = new FooNode({ a: 123 }, { id: "a" });
12 | const b = new FooNode(
13 | { b: a.future.get("x"), c: sb.concat("x", "y") },
14 | { id: "b" },
15 | );
16 |
17 | const result = Substrate.serialize(a, b);
18 |
19 | expect(result).toEqual({
20 | edges: [],
21 | initial_args: {},
22 | nodes: [
23 | {
24 | node: "FooNode",
25 | id: a.id,
26 | args: {
27 | a: 123,
28 | },
29 | _should_output_globally: true,
30 | },
31 | {
32 | node: "FooNode",
33 | id: b.id,
34 | args: {
35 | b: {
36 | __$$SB_GRAPH_OP_ID$$__: expect.stringMatching(/future/),
37 | },
38 | c: {
39 | __$$SB_GRAPH_OP_ID$$__: expect.stringMatching(/future/),
40 | },
41 | },
42 | _should_output_globally: true,
43 | },
44 | ],
45 | futures: [
46 | {
47 | id: expect.stringMatching(/future/),
48 | directive: {
49 | type: "trace",
50 | op_stack: [
51 | {
52 | future_id: null,
53 | key: "x",
54 | accessor: "attr",
55 | },
56 | ],
57 | origin_node_id: a.id,
58 | },
59 | },
60 | {
61 | id: expect.stringMatching(/future/),
62 | directive: {
63 | type: "string-concat",
64 | items: [
65 | {
66 | future_id: null,
67 | val: "x",
68 | },
69 | {
70 | future_id: null,
71 | val: "y",
72 | },
73 | ],
74 | },
75 | },
76 | ],
77 | });
78 | });
79 |
80 | test("when there are nodes and futures, but we only supply the 'final' node", () => {
81 | const a = new FooNode({ a: 123 });
82 | const b = new FooNode({ b: a.future.get("x"), c: sb.concat("x", "y") });
83 |
84 | // Here we're only supplying `b` and relying on the graph-serialiation to find `a`
85 | const result = Substrate.serialize(b);
86 |
87 | expect(result).toEqual({
88 | edges: [],
89 | initial_args: {},
90 | nodes: [
91 | {
92 | node: "FooNode",
93 | id: b.id,
94 | args: {
95 | b: {
96 | __$$SB_GRAPH_OP_ID$$__: expect.stringMatching(/future/),
97 | },
98 | c: {
99 | __$$SB_GRAPH_OP_ID$$__: expect.stringMatching(/future/),
100 | },
101 | },
102 | _should_output_globally: true,
103 | },
104 | {
105 | node: "FooNode",
106 | id: a.id,
107 | args: {
108 | a: 123,
109 | },
110 | _should_output_globally: true,
111 | },
112 | ],
113 | futures: [
114 | {
115 | id: expect.stringMatching(/future/),
116 | directive: {
117 | type: "trace",
118 | op_stack: [
119 | {
120 | future_id: null,
121 | key: "x",
122 | accessor: "attr",
123 | },
124 | ],
125 | origin_node_id: a.id,
126 | },
127 | },
128 | {
129 | id: expect.stringMatching(/future/),
130 | directive: {
131 | type: "string-concat",
132 | items: [
133 | {
134 | future_id: null,
135 | val: "x",
136 | },
137 | {
138 | future_id: null,
139 | val: "y",
140 | },
141 | ],
142 | },
143 | },
144 | ],
145 | });
146 | });
147 |
148 | test("when there are nodes and we use the `depends` key", () => {
149 | const a = new FooNode({ a: 123 }, { id: "a" });
150 | const b = new FooNode({ b: 456 }, { id: "b", depends: [a] });
151 | const c = new FooNode({ c: 789 }, { id: "c", depends: [a, b, b] }); // intentionally using b twice here
152 |
153 | const result = Substrate.serialize(a, b, c);
154 |
155 | expect(result).toEqual({
156 | edges: [
157 | ["a", "b", {}],
158 | ["a", "c", {}],
159 | ["b", "c", {}],
160 | ],
161 | initial_args: {},
162 | nodes: [
163 | {
164 | node: "FooNode",
165 | id: a.id,
166 | args: {
167 | a: 123,
168 | },
169 | _should_output_globally: true,
170 | },
171 | {
172 | node: "FooNode",
173 | id: b.id,
174 | args: {
175 | b: 456,
176 | },
177 | _should_output_globally: true,
178 | },
179 | {
180 | node: "FooNode",
181 | id: c.id,
182 | args: {
183 | c: 789,
184 | },
185 | _should_output_globally: true,
186 | },
187 | ],
188 | futures: [],
189 | });
190 | });
191 | });
192 | });
193 |
--------------------------------------------------------------------------------
/src/EventSource.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * EventSource/Server-Sent Events parser
3 | * @see https://html.spec.whatwg.org/multipage/server-sent-events.html
4 | *
5 | * Based on code from {@link https://github.com/rexxars/eventsource-parser},
6 | * which is licensed under the MIT license {@link https://github.com/rexxars/eventsource-parser/blob/main/LICENSE}
7 | *
8 | * Which is based on code from the {@link https://github.com/EventSource/eventsource | EventSource module},
9 | * which is licensed under the MIT license. And copyrighted the EventSource GitHub organisation.
10 | */
11 |
12 | /**
13 | * Creates a new EventSource parser.
14 | * @example
15 | *
16 | * // create a parser and read start reading a string
17 | * let parser = createParser()
18 | * for (const message of parser.getMessages(str)) {
19 | * // ...
20 | * }
21 | *
22 | * // if you want to reuse the parser.
23 | * parser.reset()
24 | */
25 | export function createParser() {
26 | // Processing state
27 | let isFirstChunk: boolean;
28 | let buffer: string;
29 | let startingPosition: number;
30 | let startingFieldLength: number;
31 |
32 | // Event state
33 | let eventId: string | undefined;
34 | let eventName: string | undefined;
35 | let data: string;
36 |
37 | reset();
38 | return { getMessages, reset };
39 |
40 | function reset(): void {
41 | isFirstChunk = true;
42 | buffer = "";
43 | startingPosition = 0;
44 | startingFieldLength = -1;
45 |
46 | eventId = undefined;
47 | eventName = undefined;
48 | data = "";
49 | }
50 |
51 | function* getMessages(chunk: string) {
52 | buffer = buffer ? buffer + chunk : chunk;
53 |
54 | // Strip any UTF8 byte order mark (BOM) at the start of the stream.
55 | // Note that we do not strip any non - UTF8 BOM, as eventsource streams are
56 | // always decoded as UTF8 as per the specification.
57 | if (isFirstChunk && hasBom(buffer)) {
58 | buffer = buffer.slice(BOM.length);
59 | }
60 |
61 | isFirstChunk = false;
62 |
63 | // Set up chunk-specific processing state
64 | const length = buffer.length;
65 | let position = 0;
66 | let discardTrailingNewline = false;
67 |
68 | // Read the current buffer byte by byte
69 | while (position < length) {
70 | // EventSource allows for carriage return + line feed, which means we
71 | // need to ignore a linefeed character if the previous character was a
72 | // carriage return
73 | // @todo refactor to reduce nesting, consider checking previous byte?
74 | // @todo but consider multiple chunks etc
75 | if (discardTrailingNewline) {
76 | if (buffer[position] === "\n") {
77 | ++position;
78 | }
79 | discardTrailingNewline = false;
80 | }
81 |
82 | let lineLength = -1;
83 | let fieldLength = startingFieldLength;
84 | let character: string;
85 |
86 | for (
87 | let index = startingPosition;
88 | lineLength < 0 && index < length;
89 | ++index
90 | ) {
91 | character = buffer[index] as string;
92 | if (character === ":" && fieldLength < 0) {
93 | fieldLength = index - position;
94 | } else if (character === "\r") {
95 | discardTrailingNewline = true;
96 | lineLength = index - position;
97 | } else if (character === "\n") {
98 | lineLength = index - position;
99 | }
100 | }
101 |
102 | if (lineLength < 0) {
103 | startingPosition = length - position;
104 | startingFieldLength = fieldLength;
105 | break;
106 | } else {
107 | startingPosition = 0;
108 | startingFieldLength = -1;
109 | }
110 |
111 | for (let event of parseEventStreamLine(
112 | buffer,
113 | position,
114 | fieldLength,
115 | lineLength,
116 | )) {
117 | if (event) yield event;
118 | }
119 |
120 | position += lineLength + 1;
121 | }
122 |
123 | if (position === length) {
124 | // If we consumed the entire buffer to read the event, reset the buffer
125 | buffer = "";
126 | } else if (position > 0) {
127 | // If there are bytes left to process, set the buffer to the unprocessed
128 | // portion of the buffer only
129 | buffer = buffer.slice(position);
130 | }
131 | }
132 |
133 | function* parseEventStreamLine(
134 | lineBuffer: string,
135 | index: number,
136 | fieldLength: number,
137 | lineLength: number,
138 | ) {
139 | if (lineLength === 0) {
140 | // We reached the last line of this event
141 | if (data.length > 0) {
142 | yield {
143 | type: "event",
144 | id: eventId,
145 | event: eventName || undefined,
146 | data: data.slice(0, -1), // remove trailing newline
147 | };
148 |
149 | data = "";
150 | eventId = undefined;
151 | }
152 | eventName = undefined;
153 | yield null;
154 | }
155 |
156 | const noValue = fieldLength < 0;
157 | const field = lineBuffer.slice(
158 | index,
159 | index + (noValue ? lineLength : fieldLength),
160 | );
161 | let step = 0;
162 |
163 | if (noValue) {
164 | step = lineLength;
165 | } else if (lineBuffer[index + fieldLength + 1] === " ") {
166 | step = fieldLength + 2;
167 | } else {
168 | step = fieldLength + 1;
169 | }
170 |
171 | const position = index + step;
172 | const valueLength = lineLength - step;
173 | const value = lineBuffer.slice(position, position + valueLength).toString();
174 |
175 | if (field === "data") {
176 | data += value ? `${value}\n` : "\n";
177 | } else if (field === "event") {
178 | eventName = value;
179 | } else if (field === "id" && !value.includes("\u0000")) {
180 | eventId = value;
181 | } else if (field === "retry") {
182 | const retry = parseInt(value, 10);
183 | if (!Number.isNaN(retry)) {
184 | yield { type: "reconnect-interval", value: retry };
185 | }
186 | }
187 | }
188 | }
189 |
190 | const BOM = [239, 187, 191];
191 |
192 | function hasBom(buffer: string) {
193 | return BOM.every(
194 | (charCode: number, index: number) => buffer.charCodeAt(index) === charCode,
195 | );
196 | }
197 |
--------------------------------------------------------------------------------
/src/Node.ts:
--------------------------------------------------------------------------------
1 | import { idGenerator } from "substrate/idGenerator";
2 | import { Future, FutureAnyObject, Trace } from "substrate/Future";
3 | import { SubstrateResponse } from "substrate/SubstrateResponse";
4 | import { NodeError, SubstrateError } from "substrate/Error";
5 | import { AnyNode } from "substrate/Nodes";
6 |
7 | const generator = idGenerator("node");
8 |
9 | export type Options = {
10 | /** The id of the node. Default: random id */
11 | id?: Node["id"];
12 | /** When true the server will omit this node's output. Default: false */
13 | hide?: boolean;
14 | /** Number of seconds to cache an output for this node's unique inputs. Default: null */
15 | cache_age?: number;
16 | /** Applies if cache_age > 0. Optionally specify a subset of keys to use when computing a cache key.
17 | * Default: all node arguments
18 | */
19 | cache_keys?: string[];
20 | /** Max number of times to retry this node if it fails. Default: null means no retries */
21 | max_retries?: number;
22 | /** Specify nodes that this node depends on. */
23 | depends?: Node[];
24 | };
25 |
26 | export abstract class Node {
27 | /** The id of the node. Default: random id */
28 | id: string;
29 | /** The type of the node. */
30 | node: string;
31 | /** Node inputs */
32 | args: Object;
33 | /** When true the server will omit this node's output. Default: false */
34 | hide: boolean;
35 | /** Number of seconds to cache an output for this node's unique inputs. Default: null */
36 | cache_age?: number;
37 | /** Applies if cache_age > 0. Optionally specify a subset of keys to use when computing a cache key.
38 | * Default: all node arguments
39 | */
40 | cache_keys?: string[];
41 | /** Max number of times to retry this node if it fails. Default: null means no retries */
42 | max_retries?: number;
43 | /** Specify nodes that this node depends on. */
44 | depends: Node[];
45 |
46 | /** TODO this field stores the last response, but it's just temporary until the internals are refactored */
47 | protected _response: SubstrateResponse | undefined;
48 |
49 | constructor(args: Object = {}, opts?: Options) {
50 | this.node = this.constructor.name;
51 | this.args = args;
52 | this.id = opts?.id || generator(this.node);
53 | this.hide = opts?.hide || false;
54 | this.cache_age = opts?.cache_age;
55 | this.cache_keys = opts?.cache_keys;
56 | this.max_retries = opts?.max_retries;
57 | this.depends = opts?.depends ?? [];
58 | }
59 |
60 | /**
61 | * Reference the future output of this node.
62 | */
63 | get future(): any {
64 | return new FutureAnyObject(new Trace([], this as Node));
65 | }
66 |
67 | protected set response(res: SubstrateResponse) {
68 | this._response = res;
69 | }
70 |
71 | protected output() {
72 | const data = this._response?.json?.data?.[this.id];
73 |
74 | // Errors from the server have these two fields
75 | if (data?.type && data?.message) {
76 | // NOTE: we only return these errors on client errors.
77 | // Server errors are typically 5xx replies.
78 | return new NodeError(data.type, data.message, data?.request_id);
79 | } else if (data) {
80 | return data;
81 | }
82 |
83 | return new NodeError("no_data", `Missing data for "${this.id}"`);
84 | }
85 |
86 | /**
87 | * Return the resolved result for this node.
88 | */
89 | protected async result(): Promise {
90 | if (!this._response) {
91 | return Promise.reject(
92 | new SubstrateError(
93 | `${this.node} (id=${this.id}) has not been run yet!`,
94 | ),
95 | );
96 | }
97 | return Promise.resolve(
98 | this._response
99 | ? this._response.get(this as unknown as AnyNode)
100 | : undefined,
101 | );
102 | }
103 |
104 | toJSON() {
105 | const withPlaceholders = (obj: any): any => {
106 | if (Array.isArray(obj)) {
107 | return obj.map((item) => withPlaceholders(item));
108 | }
109 |
110 | if (obj instanceof Future) {
111 | // @ts-expect-error (accessing protected method toPlaceholder)
112 | return obj.toPlaceholder();
113 | }
114 |
115 | if (obj && typeof obj === "object") {
116 | return Object.keys(obj).reduce((acc: any, k: any) => {
117 | acc[k] = withPlaceholders(obj[k]);
118 | return acc;
119 | }, {});
120 | }
121 |
122 | return obj;
123 | };
124 |
125 | return {
126 | id: this.id,
127 | node: this.node,
128 | args: withPlaceholders(this.args),
129 | _should_output_globally: !this.hide,
130 | ...(this.cache_age && { _cache_age: this.cache_age }),
131 | ...(this.cache_keys && { _cache_keys: this.cache_keys }),
132 | ...(this.max_retries && { _max_retries: this.max_retries }),
133 | };
134 | }
135 |
136 | /**
137 | * @private
138 | * For this node, return all the Futures and other Nodes it has a reference to.
139 | */
140 | protected references() {
141 | const nodes = new Set();
142 | const futures = new Set>();
143 |
144 | nodes.add(this);
145 |
146 | for (let node of this.depends) {
147 | const references = node.references();
148 | for (let node of references.nodes) {
149 | nodes.add(node);
150 | }
151 | for (let future of references.futures) {
152 | futures.add(future);
153 | }
154 | }
155 |
156 | const collectFutures = (obj: any) => {
157 | if (Array.isArray(obj)) {
158 | for (let item of obj) {
159 | collectFutures(item);
160 | }
161 | }
162 |
163 | if (obj instanceof Future) {
164 | futures.add(obj);
165 |
166 | // @ts-expect-error (accessing protected method referencedFutures)
167 | for (let future of obj.referencedFutures()) {
168 | futures.add(future);
169 | }
170 | return;
171 | }
172 |
173 | if (obj && typeof obj === "object") {
174 | for (let key of Object.keys(obj)) {
175 | collectFutures(obj[key]);
176 | }
177 | }
178 | };
179 | collectFutures(this.args);
180 |
181 | for (let future of futures) {
182 | // @ts-ignore protected access
183 | let directive = future._directive;
184 | if (directive instanceof Trace) {
185 | // @ts-ignore protected access
186 | const references = directive.originNode.references();
187 | for (let node of references.nodes) {
188 | nodes.add(node);
189 | }
190 | for (let future of references.futures) {
191 | futures.add(future);
192 | }
193 | }
194 | }
195 |
196 | return { nodes, futures };
197 | }
198 | }
199 |
--------------------------------------------------------------------------------
/examples/knowledge-graph/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {{ title }} - Relationships
7 |
8 |
56 |
57 |
58 | {{ title }}
59 |
60 |
223 |
224 |
225 |
--------------------------------------------------------------------------------
/DEVELOPMENT.md:
--------------------------------------------------------------------------------
1 | # Development Guide
2 |
3 | 👋 Hello! This guide is an aid meant for developers of this package.
4 |
5 | ## Up and Running
6 |
7 | ```sh
8 | # ensure you're using the correct node version (see .node-version)
9 |
10 | # install deps
11 | make ensure
12 |
13 | # run tests
14 | make test
15 |
16 | # build
17 | make build
18 | ```
19 |
20 | ## Generated Code
21 |
22 | There's a handful of files that we generate via our main repo for the SDK. (See `Nodes.ts`, `index.ts`, `OpenAPI.ts`)
23 |
24 | When you need to make a change to these files you will need to rerun the codegen scripts in [SubstrateLabs/substrate](https://github.com/SubstrateLabs/substrate)
25 | and then copy them into this project. Make sure to commit these changes.
26 |
27 | ```#sh
28 | # in SubstrateLabs/substrate: run the codegen tasks
29 | make generate
30 |
31 | # in SubstrateLabs/substrate-typescript: copy the generated files
32 | make sync-codegen
33 |
34 | # ensure the package builds
35 | make build
36 | ```
37 |
38 | ## Running Examples
39 |
40 | We have code in the `examples/` directory that can be used to demonstrate the SDK in action. It's a good
41 | idea to run through some of these when making changes to ensure everything works as expected end-to-end
42 | and that the docs and types are rendered as expected via editor LSP integrations.
43 |
44 | ```sh
45 | # all the examples are executable, so you can run them directly
46 |
47 | # run an example using ts-node (with typescript, using esm)
48 | ./examples/basic.ts
49 |
50 | # run an example using node (no typescript, using esm)
51 | ./examples/basic.js
52 |
53 | # run an example using node (no typescript, using commonjs)
54 | ./examples/basic.cjs
55 | ```
56 |
57 | ## Using Local Package in another Local Project
58 |
59 | Testing the local package in another local project requires making the local package available to that project.
60 | `npm` provides a simple way to do this using symlinks. (See `npm help link` for more details)
61 |
62 | ```sh
63 | # in the local SubstrateLabs/substrate-typescript directory
64 | npm link
65 |
66 | # see that the local package has been sym linked
67 | npm ls --link --global
68 |
69 | # in the local project that wants to import the local package
70 | npm link substrate
71 |
72 | # see the local project contains a link to the globally linked local package
73 | npm ls --link
74 |
75 | # remove the symlinks (if you don't want to use them anymore)
76 | npm unlink substrate # in local project
77 | npm unlink --global substrate
78 | ```
79 |
80 | ## Build Config Highlights
81 |
82 | This project uses a few tools to help with building and packaging it up: `typescript`, [`tsup`](https://github.com/egoist/tsup), and `npm`.
83 | Their respective configuration files work together to produce the the library and this section contains a few highlights to
84 | help better understand how it works.
85 |
86 | - Compiled and bundled up code lives in `dist/` (set in `tsconfig.json` "outDir")
87 | - We're targeting ES2020 as our output language, which is supprted by Node 16+ (see `tsconfig.json` "target")
88 | - We're building both ESM (.js) and CJS (.cjs) compatible artifacts (see `tsup.config.ts` "formats")
89 | - During build we're generating TypeScript declaration files and making them available to package consumers (see `tsup.config.ts` "dts")
90 | - Our package uses conditional exports so that the consumer will automatically get the right ESM or CJS based on how they import (see `package.json` "exports")
91 | - Additionally we're using conditional exports to insert polyfills for `node` environments because we're targeting Node 16 (see `src/nodejs/index.ts`)
92 | - When we publish the package to NPM we're only including the `dist/`, `src/` and "default files", eg. `README`, `LICENCE` (see `package.json` "files")
93 |
94 | ## Versioning
95 |
96 | We're using a [custom versioning scheme](https://guides.substrate.run/sdks/versioning) that allows us to
97 | incorporate information about the API version and SDK version. We update version references via the
98 | `make sync-codegen` script currently.
99 |
100 | The version string looks like the following:
101 |
102 | ```js
103 | `${major}${version}.${minor}.${patch}`;
104 | ```
105 |
106 | - `major` is set manually and is the MAJOR version of the SDK code
107 | - `version` is a date string (`yyyymmdd`) we use as the API Version (from our OpenAPI schema)
108 | - `minor` is set manually and is the MINOR version of the SDK code
109 | - `patch` is set manually and is the PATCH version of the SDK code
110 |
111 | The version of the SDK should be updated in branches being merged into `main` according to the semantic versioning scheme:
112 |
113 | - MAJOR version when you make incompatible API changes
114 | - MINOR version when you add functionality in a backward compatible manner
115 | - PATCH version when you make backward compatible bug fixes
116 |
117 | ### Updating The Package Version
118 |
119 | After making changes, you should:
120 |
121 | - Make sure to bump the `SDK_VERSION` in the `bin/update-version.ts` script
122 | - Then run `make update-version` to ensure the `package.json` and `src/version.ts` are set correctly.
123 |
124 | **NOTE:** If the API version (the date string) has changed, you should reset the minor and patch version to `0.0`.
125 |
126 | **NOTE:** The `make update-version` task will run after every `make sync-codegen` too!
127 |
128 | ## CI & Git Hooks
129 |
130 | We're using GitHub Actions for running tests, verifying successful builds, typechecking, and formatting (see `.github/workflows/node.js.yml`)
131 |
132 | Right now we're running these steps using Node 16 and Node 18, but may add more variants soon. Additionally we may automate the release process through this mechanism as well.
133 |
134 | In order to speed up the feedback you can also use the git hooks setup with [`pre-commit`](https://pre-commit.com/).
135 |
136 | ```sh
137 | # install pre-commit
138 | brew install pre-commit
139 |
140 | # install git-hooks
141 | pre-commit install
142 | ```
143 |
144 | ## Releasing
145 |
146 | **Prerequisites**:
147 |
148 | - Have an [npmjs.com](https://www.npmjs.com/) account and it must be a member of our organization
149 | - Have an GitHub account and it must be a member of our organization with write permissions on the repo
150 | - The changes that are currently in the branch are the one you would like to release (typically `main`)
151 | - The `version` field in the `package.json` is the one you would like to use for the release already
152 |
153 | **Production Releases**:
154 |
155 | We've not automated this process just yet since there are a few more steps that would be nice to incorporate once
156 | we start up a more regular cadence. The following should be done manually on the publishers machine in the branch
157 | that we'd like to publish.
158 |
159 | 1. Preview the "pack" (tarball we publish to npm): `make publish-preview`
160 | 2. Publish to NPM & push tag to GitHub: `make publish`
161 |
162 | **Non-Production Releases**
163 |
164 | The previous workflow assumes that the tag being published should be marked as `latest` for the NPM
165 | [distribution tag](https://docs.npmjs.com/adding-dist-tags-to-packages). If you would like to mark a release as
166 | a "release candidate", "experimental" or something else, then specify this tag when publishing:
167 |
168 | ```
169 | make publish NPM_TAG=[rc|experimental|etc]
170 | ```
171 |
--------------------------------------------------------------------------------
/examples/streaming/node-http-basic/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "node16-streaming",
3 | "version": "1.0.0",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "node16-streaming",
9 | "version": "1.0.0",
10 | "license": "ISC",
11 | "dependencies": {
12 | "readable-from-web": "^1.0.0"
13 | }
14 | },
15 | "node_modules/@types/node": {
16 | "version": "20.14.2",
17 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.2.tgz",
18 | "integrity": "sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==",
19 | "dependencies": {
20 | "undici-types": "~5.26.4"
21 | }
22 | },
23 | "node_modules/@types/readable-stream": {
24 | "version": "4.0.14",
25 | "resolved": "https://registry.npmjs.org/@types/readable-stream/-/readable-stream-4.0.14.tgz",
26 | "integrity": "sha512-xZn/AuUbCMShGsqH/ehZtGDwQtbx00M9rZ2ENLe4tOjFZ/JFeWMhEZkk2fEe1jAUqqEAURIkFJ7Az/go8mM1/w==",
27 | "dependencies": {
28 | "@types/node": "*",
29 | "safe-buffer": "~5.1.1"
30 | }
31 | },
32 | "node_modules/abort-controller": {
33 | "version": "3.0.0",
34 | "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz",
35 | "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==",
36 | "dependencies": {
37 | "event-target-shim": "^5.0.0"
38 | },
39 | "engines": {
40 | "node": ">=6.5"
41 | }
42 | },
43 | "node_modules/base64-js": {
44 | "version": "1.5.1",
45 | "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
46 | "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
47 | "funding": [
48 | {
49 | "type": "github",
50 | "url": "https://github.com/sponsors/feross"
51 | },
52 | {
53 | "type": "patreon",
54 | "url": "https://www.patreon.com/feross"
55 | },
56 | {
57 | "type": "consulting",
58 | "url": "https://feross.org/support"
59 | }
60 | ]
61 | },
62 | "node_modules/buffer": {
63 | "version": "6.0.3",
64 | "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
65 | "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
66 | "funding": [
67 | {
68 | "type": "github",
69 | "url": "https://github.com/sponsors/feross"
70 | },
71 | {
72 | "type": "patreon",
73 | "url": "https://www.patreon.com/feross"
74 | },
75 | {
76 | "type": "consulting",
77 | "url": "https://feross.org/support"
78 | }
79 | ],
80 | "dependencies": {
81 | "base64-js": "^1.3.1",
82 | "ieee754": "^1.2.1"
83 | }
84 | },
85 | "node_modules/event-target-shim": {
86 | "version": "5.0.1",
87 | "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz",
88 | "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==",
89 | "engines": {
90 | "node": ">=6"
91 | }
92 | },
93 | "node_modules/events": {
94 | "version": "3.3.0",
95 | "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz",
96 | "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==",
97 | "engines": {
98 | "node": ">=0.8.x"
99 | }
100 | },
101 | "node_modules/ieee754": {
102 | "version": "1.2.1",
103 | "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
104 | "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
105 | "funding": [
106 | {
107 | "type": "github",
108 | "url": "https://github.com/sponsors/feross"
109 | },
110 | {
111 | "type": "patreon",
112 | "url": "https://www.patreon.com/feross"
113 | },
114 | {
115 | "type": "consulting",
116 | "url": "https://feross.org/support"
117 | }
118 | ]
119 | },
120 | "node_modules/process": {
121 | "version": "0.11.10",
122 | "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz",
123 | "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==",
124 | "engines": {
125 | "node": ">= 0.6.0"
126 | }
127 | },
128 | "node_modules/readable-from-web": {
129 | "version": "1.0.0",
130 | "resolved": "https://registry.npmjs.org/readable-from-web/-/readable-from-web-1.0.0.tgz",
131 | "integrity": "sha512-tei03fQhxqLEklpIvocFUR9hO42hiyYvdhwoNHAjJztPAQ8QS1NqF2AhLwzGxIGidPBJ4MCqB48wn7OAFCfhsQ==",
132 | "dependencies": {
133 | "@types/readable-stream": "^4.0.0",
134 | "readable-stream": "^4.0.0"
135 | }
136 | },
137 | "node_modules/readable-stream": {
138 | "version": "4.5.2",
139 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-4.5.2.tgz",
140 | "integrity": "sha512-yjavECdqeZ3GLXNgRXgeQEdz9fvDDkNKyHnbHRFtOr7/LcfgBcmct7t/ET+HaCTqfh06OzoAxrkN/IfjJBVe+g==",
141 | "dependencies": {
142 | "abort-controller": "^3.0.0",
143 | "buffer": "^6.0.3",
144 | "events": "^3.3.0",
145 | "process": "^0.11.10",
146 | "string_decoder": "^1.3.0"
147 | },
148 | "engines": {
149 | "node": "^12.22.0 || ^14.17.0 || >=16.0.0"
150 | }
151 | },
152 | "node_modules/safe-buffer": {
153 | "version": "5.1.2",
154 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
155 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
156 | },
157 | "node_modules/string_decoder": {
158 | "version": "1.3.0",
159 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
160 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
161 | "dependencies": {
162 | "safe-buffer": "~5.2.0"
163 | }
164 | },
165 | "node_modules/string_decoder/node_modules/safe-buffer": {
166 | "version": "5.2.1",
167 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
168 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
169 | "funding": [
170 | {
171 | "type": "github",
172 | "url": "https://github.com/sponsors/feross"
173 | },
174 | {
175 | "type": "patreon",
176 | "url": "https://www.patreon.com/feross"
177 | },
178 | {
179 | "type": "consulting",
180 | "url": "https://feross.org/support"
181 | }
182 | ]
183 | },
184 | "node_modules/undici-types": {
185 | "version": "5.26.5",
186 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
187 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
188 | }
189 | }
190 | }
191 |
--------------------------------------------------------------------------------
/tests/Future.test.ts:
--------------------------------------------------------------------------------
1 | // including polyfill for node16 support
2 | import "substrate/nodejs/polyfill";
3 |
4 | import { expect, describe, test } from "vitest";
5 | import {
6 | Future,
7 | FutureString,
8 | FutureNumber,
9 | Trace,
10 | StringConcat,
11 | } from "substrate/Future";
12 | import { Node } from "substrate/Node";
13 | import { SubstrateResponse } from "substrate/SubstrateResponse";
14 |
15 | class FooFuture extends Future {}
16 | class FooNode extends Node {}
17 |
18 | const node = (id: string = "") => new FooNode({}, { id });
19 |
20 | // Helper that makes a Node and sets it's output with a fake SubstrateResponse
21 | const staticNode = (output: any) => {
22 | const node = new FooNode({});
23 | // NOTE: request not being sent, but we need to provide a valid URI here to construct a Request
24 | const req = new Request("http://127.0.0.1");
25 |
26 | const res = new SubstrateResponse(req, new Response(), {
27 | data: { [node.id]: output },
28 | });
29 |
30 | // @ts-expect-error (protected prop response)
31 | node.response = res;
32 | return node;
33 | };
34 |
35 | describe("Future", () => {
36 | test(".toJSON", () => {
37 | const d = new Trace([], node("a"));
38 | const f = new FooFuture(d, "123");
39 | expect(f.toJSON()).toEqual({
40 | id: "123",
41 | directive: d.toJSON(),
42 | });
43 | });
44 |
45 | test(".referencedFutures", () => {
46 | const a = new FutureString(new Trace([], node()));
47 | const b = new FutureString(new Trace([], node()));
48 | const c = new FutureString(new StringConcat([a, b]));
49 | const f = new FooFuture(new StringConcat([c, "d"]));
50 |
51 | // @ts-expect-error (accessing protected property)
52 | expect(f.referencedFutures()).toEqual([c, a, b]);
53 | });
54 |
55 | describe("Trace (Directive)", () => {
56 | test(".next", () => {
57 | const s = new FutureString(new Trace([], node("123")));
58 | const n = new FutureNumber(new Trace([], node("456")));
59 | const d = new Trace(["a", 1, s, n], node("NodeId"));
60 | const d2 = d.next("b", 2);
61 |
62 | expect(d2.items).toEqual(["a", 1, s, n, "b", 2]);
63 | expect(d2.originNode.id).toEqual("NodeId");
64 | });
65 |
66 | test(".result", async () => {
67 | // when the trace is empty, it resovles to the node's output
68 | const n0 = staticNode("hello");
69 | const t0 = new Trace([], n0);
70 | expect(t0.result()).resolves.toEqual("hello");
71 |
72 | // when the trace only includes primitive values
73 | const n1 = staticNode({ a: ["result1"] });
74 | const t1 = new Trace(["a", 0], n1);
75 | expect(t1.result()).resolves.toEqual("result1");
76 |
77 | // when the trace contains futures, they get resolved
78 | const fs = new FutureString(new Trace([], staticNode("b")));
79 | const fn = new FutureNumber(new Trace([], staticNode(1)));
80 | const n2 = staticNode({ a: [{ b: [undefined, "result2"] }] });
81 | const t2 = new Trace(["a", 0, fs, fn], n2);
82 | expect(t2.result()).resolves.toEqual("result2");
83 | });
84 |
85 | test(".toJSON", () => {
86 | const s = new FutureString(new Trace([], node()), "123");
87 | const n = new FutureNumber(new Trace([], node()), "456");
88 | const d = new Trace(["a", 1, s, n], node("NodeId"));
89 |
90 | expect(d.toJSON()).toEqual({
91 | type: "trace",
92 | origin_node_id: "NodeId",
93 | op_stack: [
94 | Trace.Operation.key("attr", "a"),
95 | Trace.Operation.key("item", 1),
96 | Trace.Operation.future("attr", "123"),
97 | Trace.Operation.future("item", "456"),
98 | ],
99 | });
100 | });
101 |
102 | test(".referencedFutures", () => {
103 | const s = new FutureString(new Trace([], node()));
104 | const n = new FutureNumber(new Trace([], node()));
105 | const d = new Trace(["a", 1, s, n], node("NodeId"));
106 |
107 | expect(d.referencedFutures()).toEqual([s, n]);
108 | });
109 | });
110 |
111 | describe("StringConcat (Directive)", () => {
112 | test(".next", () => {
113 | const s = new FutureString(new Trace([], node()));
114 | const s2 = new FutureString(new Trace([], node()));
115 | const d = new StringConcat(["a", s]);
116 | const d2 = d.next("b", s2);
117 |
118 | expect(d2.items).toEqual(["a", s, "b", s2]);
119 | });
120 |
121 | test(".result", async () => {
122 | // when the items are empty
123 | const s0 = new StringConcat([]);
124 | expect(s0.result()).resolves.toEqual("");
125 |
126 | // when the items only includes primitive values
127 | const s1 = new StringConcat(["a", "b"]);
128 | expect(s1.result()).resolves.toEqual("ab");
129 |
130 | // when the items includes primitive values and futures
131 | const fs = new FutureString(new Trace([], staticNode("b")));
132 | const s2 = new StringConcat(["a", fs]);
133 | expect(s2.result()).resolves.toEqual("ab");
134 | });
135 |
136 | test(".toJSON", () => {
137 | const s = new FutureString(new Trace([], node()), "123");
138 | const d = new StringConcat(["a", s]);
139 |
140 | expect(d.toJSON()).toEqual({
141 | type: "string-concat",
142 | items: [
143 | StringConcat.Concatable.string("a"),
144 | StringConcat.Concatable.future("123"),
145 | ],
146 | });
147 | });
148 |
149 | test(".referencedFutures", () => {
150 | const a = new FutureString(new Trace([], node()));
151 | const b = new FutureString(new Trace([], node()));
152 | const c = new FutureString(new StringConcat([a, b]));
153 | const d = new StringConcat([c, "d"]);
154 |
155 | expect(d.referencedFutures()).toEqual([c, a, b]);
156 | });
157 | });
158 |
159 | describe("FutureString", () => {
160 | test(".concat (static)", () => {
161 | const s = FutureString.concat("a");
162 | expect(s).toBeInstanceOf(FutureString);
163 | // @ts-expect-error (protected access)
164 | expect(s._directive).toEqual(new StringConcat(["a"]));
165 | });
166 |
167 | test(".concat", () => {
168 | const s1 = FutureString.concat("a");
169 | const s2 = s1.concat("b", "c");
170 | expect(s2).toBeInstanceOf(FutureString);
171 | // @ts-expect-error (protected access)
172 | expect(s2._directive).toEqual(new StringConcat([s1, "b", "c"]));
173 | });
174 |
175 | test(".interpolate", async () => {
176 | const world = "world";
177 | const nice = "nice";
178 | const i1 = FutureString.interpolate`hello ${world}, you look ${nice} today.`;
179 |
180 | // @ts-expect-error
181 | expect(i1._result()).resolves.toEqual(
182 | "hello world, you look nice today.",
183 | );
184 |
185 | const f1 = FutureString.concat("texas", " ", "sun");
186 | const f2 = FutureString.concat("texas", " ", "moon");
187 | const i2 = FutureString.interpolate`~~ ${f1} x ${f2} ~~`;
188 |
189 | // @ts-expect-error
190 | expect(i2._result()).resolves.toEqual("~~ texas sun x texas moon ~~");
191 | });
192 |
193 | test(".interpolate (when there is no space between interpolated items)", async () => {
194 | const a = "1";
195 | const b = "2";
196 | const i1 = FutureString.interpolate`hello${a}${b}`;
197 |
198 | // @ts-expect-error
199 | expect(i1._result()).resolves.toEqual("hello12");
200 |
201 | const f1 = FutureString.concat("1");
202 | const f2 = FutureString.concat("2");
203 | const i2 = FutureString.interpolate`hello${f1}${f2}`;
204 |
205 | // @ts-expect-error
206 | expect(i2._result()).resolves.toEqual("hello12");
207 | });
208 | });
209 | });
210 |
--------------------------------------------------------------------------------
/src/Future.ts:
--------------------------------------------------------------------------------
1 | import { idGenerator } from "substrate/idGenerator";
2 | import { Node } from "substrate/Node";
3 |
4 | type Accessor = "item" | "attr";
5 | type TraceOperation = {
6 | future_id: string | null;
7 | key: string | number | null;
8 | accessor: Accessor;
9 | };
10 |
11 | type TraceProp = string | FutureString | number | FutureNumber;
12 | type Concatable = string | FutureString;
13 | type JQCompatible = Record | any[] | string | number;
14 | type JQDirectiveTarget = Future | JQCompatible;
15 | type FutureTypeMap = {
16 | string: FutureString;
17 | object: FutureAnyObject;
18 | number: FutureNumber;
19 | boolean: FutureBoolean;
20 | };
21 | const parsePath = (path: string): TraceProp[] => {
22 | // Split the path by dots or brackets, and filter out empty strings
23 | const parts = path.split(/\.|\[|\]\[?/).filter(Boolean);
24 | // Convert numeric parts to numbers and keep others as strings
25 | return parts.map((part) => (isNaN(Number(part)) ? part : Number(part)));
26 | };
27 |
28 | const newFutureId = idGenerator("future");
29 |
30 | abstract class Directive {
31 | abstract items: any[];
32 | abstract next(...args: any[]): Directive;
33 | abstract toJSON(): any;
34 |
35 | abstract result(): Promise;
36 |
37 | referencedFutures() {
38 | return (
39 | this.items
40 | .filter((p) => p instanceof Future)
41 | // @ts-ignore
42 | .flatMap((p) => [p, ...p.referencedFutures()])
43 | );
44 | }
45 | }
46 |
47 | export class Trace extends Directive {
48 | items: TraceProp[];
49 | originNode: Node;
50 |
51 | constructor(items: TraceProp[], originNode: Node) {
52 | super();
53 | this.items = items;
54 | this.originNode = originNode;
55 | }
56 |
57 | static Operation = {
58 | future: (accessor: Accessor, id: Future["_id"]) => ({
59 | future_id: id,
60 | key: null,
61 | accessor,
62 | }),
63 | key: (accessor: Accessor, key: string | number) => ({
64 | future_id: null,
65 | key,
66 | accessor,
67 | }),
68 | };
69 |
70 | override next(...items: TraceProp[]) {
71 | return new Trace([...this.items, ...items], this.originNode);
72 | }
73 |
74 | override async result(): Promise {
75 | // @ts-expect-error (protected result())
76 | let result: any = await this.originNode.result();
77 |
78 | for (let item of this.items) {
79 | if (item instanceof Future) {
80 | // @ts-expect-error (protected result())
81 | item = await item._result();
82 | }
83 | result = result[item as string | number];
84 | }
85 | return result;
86 | }
87 |
88 | override toJSON() {
89 | return {
90 | type: "trace",
91 | origin_node_id: this.originNode.id,
92 | op_stack: this.items.map((item) => {
93 | if (item instanceof FutureString) {
94 | // @ts-expect-error (accessing protected prop: _id)
95 | return Trace.Operation.future("attr", item._id);
96 | } else if (item instanceof FutureNumber) {
97 | // @ts-expect-error (accessing protected prop: _id)
98 | return Trace.Operation.future("item", item._id);
99 | } else if (typeof item === "string") {
100 | return Trace.Operation.key("attr", item);
101 | }
102 | return Trace.Operation.key("item", item);
103 | }) as TraceOperation[],
104 | };
105 | }
106 | }
107 |
108 | export class JQ extends Directive {
109 | items: any[];
110 | target: JQDirectiveTarget;
111 | query: string;
112 |
113 | constructor(query: string, target: JQDirectiveTarget) {
114 | super();
115 | this.items = [target];
116 | this.target = target;
117 | this.query = query;
118 | }
119 |
120 | static JQDirectiveTarget = {
121 | future: (id: Future["_id"]) => ({ future_id: id, val: null }),
122 | rawValue: (val: JQCompatible) => ({ future_id: null, val }),
123 | };
124 |
125 | override next(...items: TraceProp[]) {
126 | return new JQ(this.query, this.target);
127 | }
128 |
129 | override async result(): Promise {
130 | return this.target instanceof Future
131 | ? // @ts-expect-error (accessing protected prop: id)
132 | await this.target._result()
133 | : this.target;
134 | }
135 |
136 | override toJSON(): any {
137 | return {
138 | type: "jq",
139 | query: this.query,
140 | target:
141 | this.target instanceof Future
142 | ? // @ts-expect-error (accessing protected prop: id)
143 | JQ.JQDirectiveTarget.future(this.target._id)
144 | : JQ.JQDirectiveTarget.rawValue(this.target),
145 | };
146 | }
147 | }
148 |
149 | export class StringConcat extends Directive {
150 | items: Concatable[];
151 |
152 | constructor(items: Concatable[] = []) {
153 | super();
154 | this.items = items;
155 | }
156 |
157 | static Concatable = {
158 | string: (val: string) => ({ future_id: null, val }),
159 | future: (id: Future["_id"]) => ({ future_id: id, val: null }),
160 | };
161 |
162 | override next(...items: Concatable[]) {
163 | return new StringConcat([...this.items, ...items]);
164 | }
165 |
166 | override async result(): Promise {
167 | let result = "";
168 | for (let item of this.items) {
169 | if (item instanceof Future) {
170 | // @ts-expect-error (protected result())
171 | item = await item._result();
172 | }
173 | result = result.concat(item);
174 | }
175 | return result;
176 | }
177 |
178 | override toJSON(): any {
179 | return {
180 | type: "string-concat",
181 | items: this.items.map((item) => {
182 | if (item instanceof Future) {
183 | // @ts-expect-error (accessing protected prop: _id)
184 | return StringConcat.Concatable.future(item._id);
185 | }
186 | return StringConcat.Concatable.string(item);
187 | }),
188 | };
189 | }
190 | }
191 |
192 | export abstract class Future {
193 | protected _directive: Directive;
194 | protected _id: string = "";
195 |
196 | constructor(directive: Directive, id: string = newFutureId()) {
197 | this._directive = directive;
198 | this._id = id;
199 | }
200 |
201 | protected referencedFutures(): Future[] {
202 | return this._directive.referencedFutures();
203 | }
204 |
205 | protected toPlaceholder() {
206 | return { __$$SB_GRAPH_OP_ID$$__: this._id };
207 | }
208 |
209 | protected async _result(): Promise {
210 | return this._directive.result();
211 | }
212 |
213 | static jq(
214 | future: JQDirectiveTarget,
215 | query: string,
216 | futureType: keyof FutureTypeMap = "string",
217 | ): FutureTypeMap[T] {
218 | const directive = new JQ(query, future);
219 | switch (futureType) {
220 | case "string":
221 | return new FutureString(directive) as FutureTypeMap[T];
222 | case "number":
223 | return new FutureNumber(directive) as FutureTypeMap[T];
224 | case "object":
225 | return new FutureAnyObject(directive) as FutureTypeMap[T];
226 | case "boolean":
227 | return new FutureBoolean(directive) as FutureTypeMap[T];
228 | default:
229 | throw new Error(`Unknown future type: ${futureType}`);
230 | }
231 | }
232 |
233 | toJSON() {
234 | return {
235 | id: this._id,
236 | directive: this._directive.toJSON(),
237 | };
238 | }
239 | }
240 |
241 | export class FutureBoolean extends Future {}
242 |
243 | export class FutureString extends Future {
244 | static concat(...items: (string | FutureString)[]) {
245 | return new FutureString(new StringConcat(items));
246 | }
247 |
248 | static interpolate(
249 | strings: TemplateStringsArray,
250 | ...exprs: ({ toString(): string } | FutureString)[]
251 | ): FutureString {
252 | return FutureString.concat(
253 | ...strings.flatMap((s: string, i: number) => {
254 | const expr = exprs[i];
255 | return expr
256 | ? [s, expr instanceof Future ? expr : expr.toString()]
257 | : [s];
258 | }),
259 | );
260 | }
261 |
262 | concat(...items: (string | FutureString)[]) {
263 | return FutureString.concat(...[this, ...items]);
264 | }
265 |
266 | protected override async _result(): Promise {
267 | return super._result();
268 | }
269 | }
270 |
271 | export class FutureNumber extends Future {}
272 |
273 | export abstract class FutureArray extends Future {
274 | abstract at(index: number): Future;
275 |
276 | protected override async _result(): Promise {
277 | return super._result();
278 | }
279 | }
280 |
281 | export abstract class FutureObject extends Future