mirror of
https://github.com/hanzoai/base.git
synced 2026-10-21 03:41:50 +00:00
7-phase implementation of Convex-inspired realtime developer experience: - crdt/: Go CRDT engine (GCounter, PNCounter, LWWRegister, ORSet, RGA text) with state-vector sync protocol and convergence-tested types (31 tests) - apis/ws.go: Pure RFC 6455 WebSocket endpoint with JWT auth, CRDT sync, presence tracking, channel subscriptions - apis/presence.go: Channel-based presence manager for collaborative awareness - cmd/typegen/: CLI tool to generate TypeScript interfaces from Base schemas (4 tests, all 14 field types mapped) - plugins/scheduler/: Scheduled functions with exactly-once semantics, retry with backoff, JSVM bindings (8 tests) - plugins/platform/: Multi-tenant platform with IAM (hanzo.id) auth, KMS (kms.hanzo.ai) secrets, tenant isolation via collection prefixes, RBAC roles, API key management (7 tests) - sdk/base-js/: @hanzoai/base TypeScript SDK with reactive queries, query deduplication, optimistic updates, state versioning, CRDT client (72 smoke tests passing) - sdk/base-react/: @hanzoai/base-react React hooks (useQuery, useMutation, useAuth, useRealtime, useCRDT, usePresence) with useSyncExternalStore
167 lines
4.1 KiB
Go
167 lines
4.1 KiB
Go
package scheduler
|
|
|
|
import (
|
|
"time"
|
|
|
|
"github.com/dop251/goja"
|
|
"github.com/hanzoai/base/core"
|
|
)
|
|
|
|
// RegisterJSVM registers the scheduler JS bindings into a goja runtime.
|
|
//
|
|
// This adds the following global functions:
|
|
//
|
|
// - scheduleAfter(delayMs, functionName, args?) - schedule execution after a delay
|
|
// - scheduleAt(isoDateString, functionName, args?) - schedule execution at a specific time
|
|
// - cancelScheduled(scheduledId) - cancel a pending scheduled function
|
|
// - listScheduled(status?) - list scheduled functions, optionally filtered by status
|
|
//
|
|
// Example JS usage:
|
|
//
|
|
// onRecordAfterCreateSuccess((e) => {
|
|
// scheduleAfter(0, "sendNotification", { recordId: e.record.id })
|
|
// }, "tasks")
|
|
//
|
|
// cronAdd("cleanup", "0 3 * * *", () => {
|
|
// scheduleAfter(0, "cleanupExpired", {})
|
|
// })
|
|
//
|
|
// // Schedule something 30 minutes from now
|
|
// scheduleAfter(1800000, "generateReport", { type: "daily" })
|
|
//
|
|
// // Schedule at a specific time
|
|
// scheduleAt("2026-03-01T09:00:00Z", "sendReminder", { userId: "abc123" })
|
|
//
|
|
// // Cancel a scheduled function
|
|
// cancelScheduled("scheduled_record_id")
|
|
//
|
|
// // List all pending scheduled functions
|
|
// const pending = listScheduled("pending")
|
|
func RegisterJSVM(app core.App, vm *goja.Runtime) {
|
|
vm.Set("scheduleAfter", func(call goja.FunctionCall) goja.Value {
|
|
delayMs := int64(0)
|
|
if arg := call.Argument(0).Export(); arg != nil {
|
|
switch v := arg.(type) {
|
|
case int64:
|
|
delayMs = v
|
|
case float64:
|
|
delayMs = int64(v)
|
|
}
|
|
}
|
|
|
|
functionName := ""
|
|
if arg := call.Argument(1).Export(); arg != nil {
|
|
if s, ok := arg.(string); ok {
|
|
functionName = s
|
|
}
|
|
}
|
|
if functionName == "" {
|
|
panic(vm.NewGoError(errMissingFunctionName))
|
|
}
|
|
|
|
var args any
|
|
if arg := call.Argument(2); arg != nil && !goja.IsUndefined(arg) && !goja.IsNull(arg) {
|
|
args = arg.Export()
|
|
}
|
|
|
|
id, err := ScheduleAfter(app, delayMs, functionName, args)
|
|
if err != nil {
|
|
panic(vm.NewGoError(err))
|
|
}
|
|
|
|
return vm.ToValue(id)
|
|
})
|
|
|
|
vm.Set("scheduleAt", func(call goja.FunctionCall) goja.Value {
|
|
isoDate := ""
|
|
if arg := call.Argument(0).Export(); arg != nil {
|
|
if s, ok := arg.(string); ok {
|
|
isoDate = s
|
|
}
|
|
}
|
|
if isoDate == "" {
|
|
panic(vm.NewGoError(errMissingTimestamp))
|
|
}
|
|
|
|
t, err := time.Parse(time.RFC3339, isoDate)
|
|
if err != nil {
|
|
// fallback: try parsing as datetime string (YYYY-MM-DD HH:MM:SS)
|
|
t, err = time.Parse("2006-01-02 15:04:05", isoDate)
|
|
if err != nil {
|
|
panic(vm.NewGoError(err))
|
|
}
|
|
}
|
|
|
|
functionName := ""
|
|
if arg := call.Argument(1).Export(); arg != nil {
|
|
if s, ok := arg.(string); ok {
|
|
functionName = s
|
|
}
|
|
}
|
|
if functionName == "" {
|
|
panic(vm.NewGoError(errMissingFunctionName))
|
|
}
|
|
|
|
var args any
|
|
if arg := call.Argument(2); arg != nil && !goja.IsUndefined(arg) && !goja.IsNull(arg) {
|
|
args = arg.Export()
|
|
}
|
|
|
|
id, err := ScheduleAt(app, t, functionName, args)
|
|
if err != nil {
|
|
panic(vm.NewGoError(err))
|
|
}
|
|
|
|
return vm.ToValue(id)
|
|
})
|
|
|
|
vm.Set("cancelScheduled", func(call goja.FunctionCall) goja.Value {
|
|
id := ""
|
|
if arg := call.Argument(0).Export(); arg != nil {
|
|
if s, ok := arg.(string); ok {
|
|
id = s
|
|
}
|
|
}
|
|
if id == "" {
|
|
panic(vm.NewGoError(errMissingScheduledId))
|
|
}
|
|
|
|
if err := CancelScheduled(app, id); err != nil {
|
|
panic(vm.NewGoError(err))
|
|
}
|
|
|
|
return goja.Undefined()
|
|
})
|
|
|
|
vm.Set("listScheduled", func(call goja.FunctionCall) goja.Value {
|
|
status := ""
|
|
if arg := call.Argument(0); arg != nil && !goja.IsUndefined(arg) && !goja.IsNull(arg) {
|
|
if s, ok := arg.Export().(string); ok {
|
|
status = s
|
|
}
|
|
}
|
|
|
|
records, err := ListScheduled(app, status)
|
|
if err != nil {
|
|
panic(vm.NewGoError(err))
|
|
}
|
|
|
|
return vm.ToValue(records)
|
|
})
|
|
}
|
|
|
|
// sentinel errors for JS binding argument validation
|
|
var (
|
|
errMissingFunctionName = &schedulerError{"scheduleAfter/scheduleAt requires a non-empty functionName"}
|
|
errMissingTimestamp = &schedulerError{"scheduleAt requires a non-empty ISO date string"}
|
|
errMissingScheduledId = &schedulerError{"cancelScheduled requires a non-empty scheduled record id"}
|
|
)
|
|
|
|
type schedulerError struct {
|
|
message string
|
|
}
|
|
|
|
func (e *schedulerError) Error() string {
|
|
return e.message
|
|
}
|