@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 withmodels
andactions
, or a string containing a JS module which exportsmodels
andactions
.topic
takes a stringsnapshot
(optional) takes aSnapshot
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.
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
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
}