Skip to content

@canvas-js/core

This package exports a Canvas class that can be used to manually instantiate Canvas applications.

Use this package directly if you want fine-grained control over when an application is started/stopped. Otherwise, you can use useCanvas in @canvas-js/hooks, which has the same API, but handles initialization inside React for you.

Table of Contents

Installation

$ npm i @canvas-js/core

How it works

Under the hood, each Canvas application replicates and executes a log of signed actions, sourced from GossipLog, with read/write access to a ModelDB database.

Each application accepts several arguments:

  • contract takes an object with models and actions, or a string containing a JS module which exports models and actions.
  • topic takes a string
  • snapshot (optional) takes a Snapshot object which provides initial database contents for the application.
  • signers (optional) takes an array of signers, which allows different auth methods to be added to the application.

Use await Canvas.initialize to start the application. (For synchronous initialization, see CanvasLoadable.ts.)

To connect the application to other sync peers, use app.connect() to start a WebSocket connection, or app.listen() to listen for WebSocket connections from the server.

Or, use app.startLibp2p() to start a libp2p node.

After starting the application, you can use app.actions to access each of the actions that you have defined.

Action calls will be signed and proxied to the contract.

ts
import { Canvas } from "@canvas-js/core"

const app = await Canvas.initialize({
  topic: "com.example.my-app",
  contract: {
    models: {
      posts: {
        id: "primary",
        user: "string",
        content: "string",
        updated_at: "integer",
      },
    },
    actions: {
      async createPost({ content }) {
        const { id, chain, address, timestamp } = this
        const user = [chain, address].join(":")
        await db.posts.set({ id, user, content, updated_at: timestamp })
      },
      async deletePost({ postId }) {
        const { chain, address } = this
        const post = await db.posts.get(postId)
        if (post === null) {
          return
        }

        const user = [chain, address].join(":")
        if (post.user !== user) {
          throw new Error("not authorized")
        }

        await db.posts.delete(postId)
      },
    },
  },
})

await app.actions.createPost({ content: "hello world!" })
const results = await app.db.query("posts", {})
// [
//   {
//     id: '09p5qn7affkhtbflscr663tet8ddeu41',
//     user: 'did:pkh:eip155:1:0x79c5158f81ebb0c2bcF877E9e1813aed2Eb652B7',
//     content: 'hello world!',
//     updated_at: 1698339861041
//   }
// ]

API

ts
import type { ModelValue } from "@canvas-js/modeldb"
import type { Awaitable } from "@canvas-js/interfaces"

export type { ModelValue } from "@canvas-js/modeldb"
import type {
	ModelInit as DbModelInit,
	ModelSchema as DbModelSchema,
	DeriveModelTypes as DbDeriveModelTypes,
	DeriveModelType as DbDeriveModelType,
} from "@canvas-js/modeldb"

export type RulesInit = { create: string | boolean; update: string | boolean; delete: string | boolean }
export type ModelInit = DbModelInit<{ $rules?: RulesInit }>
export type ModelSchema = DbModelSchema<{ $rules?: RulesInit }>
export type DeriveModelType<T extends ModelSchema> = DbDeriveModelType<T, { $rules?: RulesInit }>
export type DeriveModelTypes<T extends ModelSchema> = DbDeriveModelTypes<T, { $rules?: RulesInit }>

export type DeriveActions<T extends ModelSchema> = {
	[K in keyof T as T[K] extends { $rules: any } ? `create${Capitalize<string & K>}` : never]: (
		item: Partial<DeriveModelTypes<T>[K]>,
	) => Promise<void>
} & {
	[K in keyof T as T[K] extends { $rules: any } ? `update${Capitalize<string & K>}` : never]: (
		item: DeriveModelTypes<T>[K],
	) => Promise<void>
} & {
	[K in keyof T as T[K] extends { $rules: any } ? `delete${Capitalize<string & K>}` : never]: (
		id: string,
	) => Promise<void>
}

export type Contract<
	ModelsT extends ModelSchema = ModelSchema,
	ActionsT extends Actions<ModelsT> = Actions<ModelsT>,
> = {
	models: ModelsT
	actions?: ActionsT
}

export type Actions<ModelsT extends ModelSchema> = Record<string, ActionImplementation<ModelsT>>

export type ActionImplementation<
	ModelsT extends ModelSchema = ModelSchema,
	Args extends Array<any> = any,
	Result = any,
> = (this: ActionContext<DeriveModelTypes<ModelsT>>, ...args: Args) => Awaitable<Result>

export type ModelAPI<ModelTypes extends Record<string, ModelValue>> = {
	id: () => string
	random: () => number
	get: <T extends keyof ModelTypes & string>(model: T, key: string) => Promise<ModelTypes[T] | null>
	set: <T extends keyof ModelTypes & string>(model: T, value: ModelTypes[T]) => Promise<void>
	delete: <T extends keyof ModelTypes & string>(model: T, key: string) => Promise<void>

	create: <T extends keyof ModelTypes & string>(model: T, value: ModelTypes[T]) => Promise<void>
	update: <T extends keyof ModelTypes & string>(model: T, value: Partial<ModelTypes[T]>) => Promise<void>
	merge: <T extends keyof ModelTypes & string>(model: T, value: Partial<ModelTypes[T]>) => Promise<void>

	link: <T extends keyof ModelTypes & string>(
		modelPath: `${T}.${string}`,
		source: string,
		target: string,
	) => Promise<void>
	unlink: <T extends keyof ModelTypes & string>(
		modelPath: `${T}.${string}`,
		source: string,
		target: string,
	) => Promise<void>

	transaction: <T>(callback: () => Awaitable<T>) => Promise<T>
}

export type ActionContext<T extends Record<string, ModelValue>> = {
	db: ModelAPI<T>
	id: string
	did: string
	address: string
	blockhash: string | null
	timestamp: number
	publicKey: string
}