@pylonsync/functions 0.22.39 → 0.22.41
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +1 -1
- package/dist/types.d.ts +25 -0
- package/package.json +1 -1
- package/src/index.ts +1 -0
- package/src/runtime.ts +38 -0
- package/src/ssr-client-bundler.test.ts +87 -0
- package/src/ssr-client-bundler.ts +67 -8
- package/src/types.ts +26 -0
package/dist/index.d.ts
CHANGED
|
@@ -27,4 +27,4 @@ export type { WorkflowDefinition, WorkflowRun, WorkflowRunRequest, WorkflowRunne
|
|
|
27
27
|
export { resetDb, installTestIsolation } from "./testing";
|
|
28
28
|
export { slugifyName, availableSlug } from "./slugify";
|
|
29
29
|
export type { SsrResponse, SsrCookieOptions, SsrMetadata, Sitemap, SitemapEntry, Robots, RobotsRule, } from "./ssr-runtime";
|
|
30
|
-
export type { QueryCtx, MutationCtx, ActionCtx, DbReader, DbWriter, Stream, Scheduler, AuthInfo, AuthMode, AuthRequirement, FnDefinition, Validator, AnyValidator, ValidatorSchema, InferValidator, InferArgs, RequireMember, RequireMemberOptions, Shards, ShardsReader, ShardsWriter, ShardInfo, ShardTransfer, MemberRow, Workflows, WorkflowRunSummary, WorkflowRunStatus, Audit, AuditLog, AuditEntry, Agents, AgentRunOptions, VectorSearchQuery, VectorSearchResult, SearchResult, PaginationResult, Llm, LlmMessage, LlmContentBlock, LlmTool, LlmCompleteRequest, LlmCompleteResponse, LlmStreamEvent, Rooms, Domains, TenantDomainResult, TenantDomainRecord, TenantDomainDns, DomainAvailability, DomainContact, RegisterDomainOptions, RegisteredDomainResult, Files, ActionFiles, StoreFileOptions, StoredFile, } from "./types";
|
|
30
|
+
export type { QueryCtx, MutationCtx, ActionCtx, DbReader, DbWriter, Stream, Scheduler, AuthInfo, AuthMode, AuthRequirement, FnDefinition, Validator, AnyValidator, ValidatorSchema, InferValidator, InferArgs, RequireMember, RequireMemberOptions, Shards, ShardsReader, ShardsWriter, ShardInfo, ShardTransfer, MemberRow, Workflows, WorkflowRunSummary, WorkflowRunStatus, Audit, AuditLog, AuditEntry, Agents, AgentRunOptions, VectorSearchQuery, VectorSearchResult, SearchResult, PaginationResult, Llm, LlmMessage, LlmContentBlock, LlmTool, LlmCompleteRequest, LlmCompleteResponse, LlmStreamEvent, Rooms, Domains, TenantDomainResult, TenantDomainRecord, TenantDomainDns, DomainAvailability, DomainContact, RegisterDomainOptions, RegisteredDomainResult, Files, ActionFiles, Crdt, StoreFileOptions, StoredFile, } from "./types";
|
package/dist/types.d.ts
CHANGED
|
@@ -949,6 +949,29 @@ export type RequireMember = (orgId: string, opts?: RequireMemberOptions) => Prom
|
|
|
949
949
|
* });
|
|
950
950
|
* ```
|
|
951
951
|
*/
|
|
952
|
+
/**
|
|
953
|
+
* `ctx.crdt` (actions only): read and change a row's live CRDT document, the
|
|
954
|
+
* one editors bind to with `@pylonsync/loro`. Bytes are Loro's own formats.
|
|
955
|
+
* `@pylonsync/loro/server` wraps this in a `LoroDoc` (`openLiveDoc`).
|
|
956
|
+
*
|
|
957
|
+
* Each `apply` is its own write: it merges, then reaches everyone editing at
|
|
958
|
+
* once, so it cannot roll back with a mutation (queries and mutations get
|
|
959
|
+
* CRDT_ACTIONS_ONLY). Policies follow the `ctx.db` rule: they apply under
|
|
960
|
+
* strict function policies (`PYLON_STRICT_FN_POLICIES=1`), except through
|
|
961
|
+
* `ctx.crdt.unsafe`. Without strict policies, check access yourself.
|
|
962
|
+
*/
|
|
963
|
+
export interface Crdt {
|
|
964
|
+
/** The row's whole document, or null when the row does not exist (or, under
|
|
965
|
+
* strict policies, the caller may not read it). */
|
|
966
|
+
snapshot(entity: string, id: string): Promise<Uint8Array | null>;
|
|
967
|
+
/** The changes since `version` (a Loro version vector, `doc.oplogVersion().encode()`),
|
|
968
|
+
* or null as for {@link Crdt.snapshot}. */
|
|
969
|
+
since(entity: string, id: string, version: Uint8Array): Promise<Uint8Array | null>;
|
|
970
|
+
/** Merge a Loro update into the row and broadcast it. */
|
|
971
|
+
apply(entity: string, id: string, update: Uint8Array): Promise<void>;
|
|
972
|
+
/** The same operations with server trust even under strict policies. */
|
|
973
|
+
unsafe: Omit<Crdt, "unsafe">;
|
|
974
|
+
}
|
|
952
975
|
export interface Files {
|
|
953
976
|
/**
|
|
954
977
|
* Mint a signed download path for a file id. `ttlSecs` defaults to 300
|
|
@@ -1422,6 +1445,8 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
|
|
|
1422
1445
|
env: Record<string, string>;
|
|
1423
1446
|
/** Signed file-download URLs — see {@link Files}. */
|
|
1424
1447
|
files: ActionFiles;
|
|
1448
|
+
/** A row's live CRDT document, as Loro bytes — see {@link Crdt}. */
|
|
1449
|
+
crdt: Crdt;
|
|
1425
1450
|
/** Shard tickets, reads, and start/stop — see {@link Shards}. */
|
|
1426
1451
|
shards: Shards;
|
|
1427
1452
|
/** Run a registered query within its own read transaction. */
|
package/package.json
CHANGED
package/src/index.ts
CHANGED
package/src/runtime.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type {
|
|
|
24
24
|
EmailSender,
|
|
25
25
|
Files,
|
|
26
26
|
ActionFiles,
|
|
27
|
+
Crdt,
|
|
27
28
|
StoredFile,
|
|
28
29
|
Shards,
|
|
29
30
|
ShardInfo,
|
|
@@ -673,6 +674,36 @@ function buildActionFiles(callId: string): ActionFiles {
|
|
|
673
674
|
* FILES_STORE_MAX_BYTES; `PYLON_MAX_UPLOAD_BYTES` can lower the host's. */
|
|
674
675
|
const FILES_STORE_MAX_BYTES = 25 * 1024 * 1024;
|
|
675
676
|
|
|
677
|
+
/** `ctx.crdt` — see {@link Crdt}. */
|
|
678
|
+
function buildCrdt(callId: string): Crdt {
|
|
679
|
+
const ops = (unsafeOp: boolean): Omit<Crdt, "unsafe"> => {
|
|
680
|
+
const call = (entity: string, id: string, op: Record<string, unknown>) =>
|
|
681
|
+
rpcDb(callId, { type: "crdt_op", entity, id, unsafe_op: unsafeOp, ...op }) as Promise<
|
|
682
|
+
Record<string, string> | null
|
|
683
|
+
>;
|
|
684
|
+
return {
|
|
685
|
+
async snapshot(entity, id) {
|
|
686
|
+
const res = await call(entity, id, { op: "snapshot" });
|
|
687
|
+
return res ? fromBase64(res.snapshot) : null;
|
|
688
|
+
},
|
|
689
|
+
async since(entity, id, version) {
|
|
690
|
+
const res = await call(entity, id, { op: "since", version_base64: toBase64(version) });
|
|
691
|
+
return res ? fromBase64(res.update) : null;
|
|
692
|
+
},
|
|
693
|
+
async apply(entity, id, update) {
|
|
694
|
+
await call(entity, id, { op: "apply", update_base64: toBase64(update) });
|
|
695
|
+
},
|
|
696
|
+
};
|
|
697
|
+
};
|
|
698
|
+
return { ...ops(false), unsafe: ops(true) };
|
|
699
|
+
}
|
|
700
|
+
|
|
701
|
+
/** Bytes of a base64 string. */
|
|
702
|
+
function fromBase64(data: string): Uint8Array {
|
|
703
|
+
const buf = Buffer.from(data, "base64");
|
|
704
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
705
|
+
}
|
|
706
|
+
|
|
676
707
|
/** Base64 of bytes, or of a string's UTF-8 encoding. */
|
|
677
708
|
function toBase64(data: Uint8Array | ArrayBuffer | string): string {
|
|
678
709
|
const bytes =
|
|
@@ -1445,6 +1476,7 @@ function buildActionCtx(
|
|
|
1445
1476
|
return err;
|
|
1446
1477
|
},
|
|
1447
1478
|
files: buildActionFiles(callId),
|
|
1479
|
+
crdt: buildCrdt(callId),
|
|
1448
1480
|
shards: buildShards(callId),
|
|
1449
1481
|
// Actions have no ctx.db; read membership via the built-in internal query.
|
|
1450
1482
|
requireMember: makeRequireMember(auth.userId, (entity, filter) =>
|
|
@@ -1640,6 +1672,9 @@ async function handleCall(msg: CallMessage): Promise<void> {
|
|
|
1640
1672
|
// so a call from a query or mutation gets the host's
|
|
1641
1673
|
// FILES_WRITE_NOT_ALLOWED instead of a TypeError.
|
|
1642
1674
|
files: buildActionFiles(msg.call_id),
|
|
1675
|
+
// Not on the query or mutation type; present at runtime so a call
|
|
1676
|
+
// gets the host's CRDT_ACTIONS_ONLY instead of a TypeError.
|
|
1677
|
+
crdt: buildCrdt(msg.call_id),
|
|
1643
1678
|
shards: buildShards(msg.call_id),
|
|
1644
1679
|
};
|
|
1645
1680
|
break;
|
|
@@ -1669,6 +1704,9 @@ async function handleCall(msg: CallMessage): Promise<void> {
|
|
|
1669
1704
|
// so a call from a query or mutation gets the host's
|
|
1670
1705
|
// FILES_WRITE_NOT_ALLOWED instead of a TypeError.
|
|
1671
1706
|
files: buildActionFiles(msg.call_id),
|
|
1707
|
+
// Not on the query or mutation type; present at runtime so a call
|
|
1708
|
+
// gets the host's CRDT_ACTIONS_ONLY instead of a TypeError.
|
|
1709
|
+
crdt: buildCrdt(msg.call_id),
|
|
1672
1710
|
shards: buildShards(msg.call_id),
|
|
1673
1711
|
};
|
|
1674
1712
|
break;
|
|
@@ -335,6 +335,93 @@ describe("ssr-client-bundler (Phase 1.5e)", () => {
|
|
|
335
335
|
// inlines; the build test guards the boundary wiring + not-found.tsx entry.
|
|
336
336
|
// ---------------------------------------------------------------------------
|
|
337
337
|
|
|
338
|
+
describe("imported assets", () => {
|
|
339
|
+
// loro-crdt's browser build imports its .wasm; Bun turns that into a URL
|
|
340
|
+
// string. It must be absolute under the build prefix and name a file the
|
|
341
|
+
// build wrote, or the page fetches it relative to its own path and 404s.
|
|
342
|
+
test("an imported .wasm gets an absolute URL that exists in the build", async () => {
|
|
343
|
+
tempDir = makeFixture(
|
|
344
|
+
{
|
|
345
|
+
"page.tsx": PAGE_BODY("Home"),
|
|
346
|
+
"hello/page.tsx": PAGE_BODY("Hello"),
|
|
347
|
+
"docs/[id]/page.tsx": `
|
|
348
|
+
import React from "react";
|
|
349
|
+
import wasmUrl from "./engine.wasm";
|
|
350
|
+
export default function Doc() {
|
|
351
|
+
return <div data-wasm={wasmUrl}>doc</div>;
|
|
352
|
+
}
|
|
353
|
+
`,
|
|
354
|
+
},
|
|
355
|
+
{ "layout.tsx": LAYOUT_BODY },
|
|
356
|
+
);
|
|
357
|
+
fs.writeFileSync(path.join(tempDir, "app", "docs", "[id]", "engine.wasm"), new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]));
|
|
358
|
+
originalCwd = process.cwd();
|
|
359
|
+
process.chdir(tempDir);
|
|
360
|
+
|
|
361
|
+
const { outdir, manifestPath } = await buildClientBundle();
|
|
362
|
+
|
|
363
|
+
const built = [
|
|
364
|
+
...fs.readdirSync(outdir).filter((n) => n.endsWith(".js")).map((n) => path.join(outdir, n)),
|
|
365
|
+
...fs.readdirSync(path.join(outdir, "chunks")).map((n) => path.join(outdir, "chunks", n)),
|
|
366
|
+
]
|
|
367
|
+
.map((f) => fs.readFileSync(f, "utf8"))
|
|
368
|
+
.join("\n");
|
|
369
|
+
const urls = built.match(/["'](\/_pylon\/build\/assets\/engine-[A-Za-z0-9]+\.wasm)["']/);
|
|
370
|
+
expect(urls).not.toBeNull();
|
|
371
|
+
const rel = urls![1].slice("/_pylon/build/".length);
|
|
372
|
+
expect(fs.existsSync(path.join(outdir, rel))).toBe(true);
|
|
373
|
+
// Every import between built files resolves to a file the build wrote,
|
|
374
|
+
// the way the browser resolves it (relative to the importing file).
|
|
375
|
+
const jsFiles = [
|
|
376
|
+
...fs.readdirSync(outdir).filter((n) => n.endsWith(".js")).map((n) => path.join(outdir, n)),
|
|
377
|
+
...fs.readdirSync(path.join(outdir, "chunks")).filter((n) => n.endsWith(".js")).map((n) => path.join(outdir, "chunks", n)),
|
|
378
|
+
];
|
|
379
|
+
for (const file of jsFiles) {
|
|
380
|
+
const src = fs.readFileSync(file, "utf8");
|
|
381
|
+
for (const m of src.matchAll(/(?:from\s*|import\s*\(?\s*)["']([^"']+\.js)["']/g)) {
|
|
382
|
+
const spec = m[1];
|
|
383
|
+
const target = spec.startsWith("/_pylon/build/")
|
|
384
|
+
? path.join(outdir, spec.slice("/_pylon/build/".length))
|
|
385
|
+
: path.resolve(path.dirname(file), spec);
|
|
386
|
+
expect(`${path.relative(outdir, file)} -> ${spec}: ${fs.existsSync(target)}`).toBe(`${path.relative(outdir, file)} -> ${spec}: true`);
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")) as PylonBundleManifest;
|
|
390
|
+
expect(manifest.public_prefix).toBe("/_pylon/build/");
|
|
391
|
+
for (const route of Object.values(manifest.routes)) {
|
|
392
|
+
expect(route.imports.length).toBeGreaterThan(0);
|
|
393
|
+
for (const imp of route.imports) expect(fs.existsSync(path.join(outdir, imp))).toBe(true);
|
|
394
|
+
}
|
|
395
|
+
});
|
|
396
|
+
});
|
|
397
|
+
|
|
398
|
+
describe("assets a stylesheet references", () => {
|
|
399
|
+
// The asset plugin answers JavaScript imports with a JavaScript module; a
|
|
400
|
+
// stylesheet's url(...) must stay with Bun, or the build fails with
|
|
401
|
+
// "Cannot import a .js file into a CSS file".
|
|
402
|
+
test("a page importing CSS that uses url() still builds", async () => {
|
|
403
|
+
tempDir = makeFixture(
|
|
404
|
+
{
|
|
405
|
+
"page.tsx": `
|
|
406
|
+
import React from "react";
|
|
407
|
+
import "./look.css";
|
|
408
|
+
export default function Home() {
|
|
409
|
+
return <div className="hero">home</div>;
|
|
410
|
+
}
|
|
411
|
+
`,
|
|
412
|
+
},
|
|
413
|
+
{ "layout.tsx": LAYOUT_BODY },
|
|
414
|
+
);
|
|
415
|
+
fs.writeFileSync(path.join(tempDir, "app", "look.css"), ".hero { background: url(./bg.png); }\n");
|
|
416
|
+
fs.writeFileSync(path.join(tempDir, "app", "bg.png"), new Uint8Array([137, 80, 78, 71, 13, 10, 26, 10]));
|
|
417
|
+
originalCwd = process.cwd();
|
|
418
|
+
process.chdir(tempDir);
|
|
419
|
+
|
|
420
|
+
const { manifestPath } = await buildClientBundle();
|
|
421
|
+
expect(fs.existsSync(manifestPath)).toBe(true);
|
|
422
|
+
});
|
|
423
|
+
});
|
|
424
|
+
|
|
338
425
|
describe("nearestBoundaryComponent (client boundary resolution)", () => {
|
|
339
426
|
const routes = new Set([
|
|
340
427
|
"web/app/not-found",
|
|
@@ -89,6 +89,18 @@ type BunBuildOutput = {
|
|
|
89
89
|
logs?: Array<{ level: string; message: string }>;
|
|
90
90
|
};
|
|
91
91
|
|
|
92
|
+
/** The part of Bun's plugin builder the client build uses. */
|
|
93
|
+
type BunPluginBuild = {
|
|
94
|
+
onResolve(
|
|
95
|
+
opts: { filter: RegExp; namespace?: string },
|
|
96
|
+
callback: (args: { path: string; importer: string }) => { path: string; namespace: string } | undefined | void,
|
|
97
|
+
): void;
|
|
98
|
+
onLoad(
|
|
99
|
+
opts: { filter: RegExp; namespace?: string },
|
|
100
|
+
callback: (args: { path: string }) => { contents: string; loader: "js" },
|
|
101
|
+
): void;
|
|
102
|
+
};
|
|
103
|
+
|
|
92
104
|
declare const Bun: {
|
|
93
105
|
build(opts: {
|
|
94
106
|
entrypoints: string[];
|
|
@@ -111,12 +123,7 @@ declare const Bun: {
|
|
|
111
123
|
root?: string;
|
|
112
124
|
plugins?: Array<{
|
|
113
125
|
name: string;
|
|
114
|
-
setup(build:
|
|
115
|
-
onResolve(
|
|
116
|
-
opts: { filter: RegExp; namespace?: string },
|
|
117
|
-
callback: (args: { path: string; importer: string }) => void,
|
|
118
|
-
): void;
|
|
119
|
-
}): void;
|
|
126
|
+
setup(build: BunPluginBuild): void;
|
|
120
127
|
}>;
|
|
121
128
|
}): Promise<BunBuildOutput>;
|
|
122
129
|
file(path: string): { exists(): Promise<boolean> };
|
|
@@ -1644,12 +1651,15 @@ export async function _doBuildInner(
|
|
|
1644
1651
|
naming: {
|
|
1645
1652
|
entry: "[name]-[hash].js",
|
|
1646
1653
|
chunk: "chunks/[name]-[hash].js",
|
|
1647
|
-
|
|
1654
|
+
// Bun's `[ext]` has no leading dot.
|
|
1655
|
+
asset: "assets/[name]-[hash].[ext]",
|
|
1648
1656
|
},
|
|
1657
|
+
|
|
1649
1658
|
// Refuse to bundle a server-only module into a client-reachable page —
|
|
1650
1659
|
// secrets / server config in a page's import graph would otherwise ship
|
|
1651
1660
|
// to the browser.
|
|
1652
1661
|
plugins: [
|
|
1662
|
+
absoluteAssetPlugin(outdir, fs, path, (await import("node:crypto")).createHash),
|
|
1653
1663
|
{
|
|
1654
1664
|
name: "pylon-server-only",
|
|
1655
1665
|
setup(build) {
|
|
@@ -1811,7 +1821,7 @@ export async function _doBuildInner(
|
|
|
1811
1821
|
const manifest: PylonBundleManifest = {
|
|
1812
1822
|
build_id: makeBuildId(),
|
|
1813
1823
|
outdir: outdirRel,
|
|
1814
|
-
public_prefix:
|
|
1824
|
+
public_prefix: BUILD_PREFIX,
|
|
1815
1825
|
routes: {},
|
|
1816
1826
|
...(polyfills ? { polyfills } : {}),
|
|
1817
1827
|
};
|
|
@@ -2071,6 +2081,55 @@ function makeBuildId(): string {
|
|
|
2071
2081
|
return `${process.pid.toString(36)}-${Date.now().toString(36)}-${_buildCounter}`;
|
|
2072
2082
|
}
|
|
2073
2083
|
|
|
2084
|
+
/** Where the Rust host serves the client build (`/_pylon/build/<path>`): the
|
|
2085
|
+
* manifest's `public_prefix`, and the prefix of imported assets' URLs. */
|
|
2086
|
+
const BUILD_PREFIX = "/_pylon/build/";
|
|
2087
|
+
|
|
2088
|
+
/** Files a page can import as a URL (Bun's `file` loader). */
|
|
2089
|
+
const ASSET_RE = /\.(wasm|png|jpe?g|gif|webp|avif|ico|svg|woff2?|ttf|otf|mp3|mp4|webm|wav|ogg)$/i;
|
|
2090
|
+
|
|
2091
|
+
/**
|
|
2092
|
+
* Assets JavaScript imports (a `.wasm`, an image) as absolute URLs under the
|
|
2093
|
+
* build prefix. Bun's `file` loader exports a RELATIVE path ("assets/x.wasm"),
|
|
2094
|
+
* and code that fetches it resolves it against the page: loro-crdt's browser
|
|
2095
|
+
* build fetched /dashboard/orgs/acme/assets/loro_wasm_bg-….wasm and 404'd.
|
|
2096
|
+
* Bun's `publicPath` would make it absolute, but it also rewrites imports
|
|
2097
|
+
* between split chunks and drops their `chunks/` directory (Bun 1.3.14), so
|
|
2098
|
+
* this plugin writes the file itself, named by its content hash, and exports
|
|
2099
|
+
* the absolute URL. A stylesheet's `url(...)` is left to Bun: CSS cannot
|
|
2100
|
+
* import a JavaScript module.
|
|
2101
|
+
*/
|
|
2102
|
+
function absoluteAssetPlugin(
|
|
2103
|
+
outdir: string,
|
|
2104
|
+
fs: typeof import("node:fs"),
|
|
2105
|
+
path: typeof import("node:path"),
|
|
2106
|
+
createHash: typeof import("node:crypto").createHash,
|
|
2107
|
+
): { name: string; setup(build: BunPluginBuild): void } {
|
|
2108
|
+
const NAMESPACE = "pylon-asset";
|
|
2109
|
+
return {
|
|
2110
|
+
name: "pylon-absolute-assets",
|
|
2111
|
+
setup(build) {
|
|
2112
|
+
build.onResolve({ filter: ASSET_RE }, (args) => {
|
|
2113
|
+
if (!args.importer || /\.css$/i.test(args.importer)) return undefined;
|
|
2114
|
+
const from = path.dirname(args.importer);
|
|
2115
|
+
const resolved = args.path.startsWith(".") || path.isAbsolute(args.path)
|
|
2116
|
+
? path.resolve(from, args.path)
|
|
2117
|
+
: ((Bun as any).resolveSync(args.path, from) as string);
|
|
2118
|
+
return { path: resolved, namespace: NAMESPACE };
|
|
2119
|
+
});
|
|
2120
|
+
build.onLoad({ filter: /.*/, namespace: NAMESPACE }, (args) => {
|
|
2121
|
+
const bytes = fs.readFileSync(args.path);
|
|
2122
|
+
const hash = createHash("sha256").update(bytes).digest("hex").slice(0, 12);
|
|
2123
|
+
const ext = path.extname(args.path);
|
|
2124
|
+
const rel = `assets/${path.basename(args.path, ext)}-${hash}${ext}`;
|
|
2125
|
+
fs.mkdirSync(path.join(outdir, "assets"), { recursive: true });
|
|
2126
|
+
fs.writeFileSync(path.join(outdir, rel), bytes);
|
|
2127
|
+
return { contents: `export default ${JSON.stringify(BUILD_PREFIX + rel)};`, loader: "js" };
|
|
2128
|
+
});
|
|
2129
|
+
},
|
|
2130
|
+
};
|
|
2131
|
+
}
|
|
2132
|
+
|
|
2074
2133
|
/**
|
|
2075
2134
|
* Base URL/path prepended to every SSR-emitted asset reference (the entry
|
|
2076
2135
|
* `<script type="module">`, `<link rel="stylesheet">`, and `modulepreload`s)
|
package/src/types.ts
CHANGED
|
@@ -1044,6 +1044,30 @@ export type RequireMember = (
|
|
|
1044
1044
|
* });
|
|
1045
1045
|
* ```
|
|
1046
1046
|
*/
|
|
1047
|
+
/**
|
|
1048
|
+
* `ctx.crdt` (actions only): read and change a row's live CRDT document, the
|
|
1049
|
+
* one editors bind to with `@pylonsync/loro`. Bytes are Loro's own formats.
|
|
1050
|
+
* `@pylonsync/loro/server` wraps this in a `LoroDoc` (`openLiveDoc`).
|
|
1051
|
+
*
|
|
1052
|
+
* Each `apply` is its own write: it merges, then reaches everyone editing at
|
|
1053
|
+
* once, so it cannot roll back with a mutation (queries and mutations get
|
|
1054
|
+
* CRDT_ACTIONS_ONLY). Policies follow the `ctx.db` rule: they apply under
|
|
1055
|
+
* strict function policies (`PYLON_STRICT_FN_POLICIES=1`), except through
|
|
1056
|
+
* `ctx.crdt.unsafe`. Without strict policies, check access yourself.
|
|
1057
|
+
*/
|
|
1058
|
+
export interface Crdt {
|
|
1059
|
+
/** The row's whole document, or null when the row does not exist (or, under
|
|
1060
|
+
* strict policies, the caller may not read it). */
|
|
1061
|
+
snapshot(entity: string, id: string): Promise<Uint8Array | null>;
|
|
1062
|
+
/** The changes since `version` (a Loro version vector, `doc.oplogVersion().encode()`),
|
|
1063
|
+
* or null as for {@link Crdt.snapshot}. */
|
|
1064
|
+
since(entity: string, id: string, version: Uint8Array): Promise<Uint8Array | null>;
|
|
1065
|
+
/** Merge a Loro update into the row and broadcast it. */
|
|
1066
|
+
apply(entity: string, id: string, update: Uint8Array): Promise<void>;
|
|
1067
|
+
/** The same operations with server trust even under strict policies. */
|
|
1068
|
+
unsafe: Omit<Crdt, "unsafe">;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1047
1071
|
export interface Files {
|
|
1048
1072
|
/**
|
|
1049
1073
|
* Mint a signed download path for a file id. `ttlSecs` defaults to 300
|
|
@@ -1542,6 +1566,8 @@ export interface ActionCtx<R extends AuthRequirement = "optional"> {
|
|
|
1542
1566
|
env: Record<string, string>;
|
|
1543
1567
|
/** Signed file-download URLs — see {@link Files}. */
|
|
1544
1568
|
files: ActionFiles;
|
|
1569
|
+
/** A row's live CRDT document, as Loro bytes — see {@link Crdt}. */
|
|
1570
|
+
crdt: Crdt;
|
|
1545
1571
|
/** Shard tickets, reads, and start/stop — see {@link Shards}. */
|
|
1546
1572
|
shards: Shards;
|
|
1547
1573
|
/** Run a registered query within its own read transaction. */
|