@pathmx/core 0.5.0

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.
Files changed (195) hide show
  1. package/LICENSE.md +172 -0
  2. package/README.md +111 -0
  3. package/actions/actions.ts +307 -0
  4. package/actions/draft.ts +105 -0
  5. package/actions/transaction.ts +164 -0
  6. package/assets.ts +149 -0
  7. package/authority/access.ts +820 -0
  8. package/authority/actors.ts +271 -0
  9. package/authority/agents.ts +95 -0
  10. package/authority/credentials.ts +144 -0
  11. package/authority/model.ts +71 -0
  12. package/authority/sharing.ts +89 -0
  13. package/browser-bundle.ts +61 -0
  14. package/canonical.ts +41 -0
  15. package/cli/dev.ts +134 -0
  16. package/cli/export.ts +74 -0
  17. package/cli/index.ts +52 -0
  18. package/cli/lint.ts +164 -0
  19. package/cli/mv.ts +30 -0
  20. package/cli/perms.ts +182 -0
  21. package/cli/plugin-packages.ts +159 -0
  22. package/cli/plugins.ts +95 -0
  23. package/cli/query.ts +225 -0
  24. package/cli/readme.md +95 -0
  25. package/cli/release.ts +75 -0
  26. package/cli/rm.ts +22 -0
  27. package/cli/serve.ts +100 -0
  28. package/cli/tree.ts +131 -0
  29. package/cli/update.ts +78 -0
  30. package/cli/utils.ts +53 -0
  31. package/core.ts +96 -0
  32. package/database.ts +189 -0
  33. package/dependencies.ts +60 -0
  34. package/dist/pathmx.js +1224 -0
  35. package/document.ts +194 -0
  36. package/environment.ts +110 -0
  37. package/environments/bun-plugins.ts +68 -0
  38. package/environments/bun-publish.ts +194 -0
  39. package/environments/bun.ts +223 -0
  40. package/globals.d.ts +14 -0
  41. package/graph.ts +153 -0
  42. package/host/compiler.ts +314 -0
  43. package/host/create.ts +66 -0
  44. package/host/engine.ts +630 -0
  45. package/host/plugin-host.ts +487 -0
  46. package/host/render.ts +102 -0
  47. package/host/response.ts +20 -0
  48. package/host/shell.html +15 -0
  49. package/host/source-sync.ts +254 -0
  50. package/html-attributes.ts +15 -0
  51. package/html.ts +48 -0
  52. package/icons/LICENSE +43 -0
  53. package/icons/lucide.ts +54 -0
  54. package/index.ts +75 -0
  55. package/invalidation.ts +85 -0
  56. package/log.ts +84 -0
  57. package/ops/href.ts +87 -0
  58. package/ops/index.ts +9 -0
  59. package/ops/move-source.ts +114 -0
  60. package/ops/plan.ts +47 -0
  61. package/ops/remove-source.ts +70 -0
  62. package/package.json +46 -0
  63. package/path.ts +75 -0
  64. package/pattern.ts +314 -0
  65. package/perf.ts +88 -0
  66. package/plugins/actor-path.ts +43 -0
  67. package/plugins/actor-profile.ts +55 -0
  68. package/plugins/agent-browsing.ts +183 -0
  69. package/plugins/app-component.ts +23 -0
  70. package/plugins/block-layout/index.ts +78 -0
  71. package/plugins/block-layout/theme.css +90 -0
  72. package/plugins/callout/index.ts +236 -0
  73. package/plugins/callout/theme.css +138 -0
  74. package/plugins/canonical-links.ts +144 -0
  75. package/plugins/code-highlight/client.ts +89 -0
  76. package/plugins/code-highlight/index.ts +147 -0
  77. package/plugins/code-highlight/theme.css +89 -0
  78. package/plugins/context.ts +131 -0
  79. package/plugins/datatype/Datatype[wdth,wght].woff2 +0 -0
  80. package/plugins/datatype/OFL.txt +96 -0
  81. package/plugins/datatype/index.ts +287 -0
  82. package/plugins/definition-list.ts +236 -0
  83. package/plugins/directive-definitions.ts +13 -0
  84. package/plugins/embedding/client.ts +88 -0
  85. package/plugins/embedding/index.ts +43 -0
  86. package/plugins/image/index.ts +68 -0
  87. package/plugins/image/theme.css +43 -0
  88. package/plugins/include.ts +119 -0
  89. package/plugins/index.ts +91 -0
  90. package/plugins/input/authoring.ts +236 -0
  91. package/plugins/input/config.ts +85 -0
  92. package/plugins/input/index.ts +359 -0
  93. package/plugins/input/model.ts +173 -0
  94. package/plugins/input/state.ts +272 -0
  95. package/plugins/layout.ts +313 -0
  96. package/plugins/literate/client.ts +208 -0
  97. package/plugins/literate/index.ts +742 -0
  98. package/plugins/math/index.ts +140 -0
  99. package/plugins/meta.ts +42 -0
  100. package/plugins/navigation/index.ts +158 -0
  101. package/plugins/navigation/model.ts +364 -0
  102. package/plugins/preset.ts +65 -0
  103. package/plugins/query/index.ts +322 -0
  104. package/plugins/rss/index.ts +46 -0
  105. package/plugins/runtime/actions.ts +62 -0
  106. package/plugins/runtime/client.ts +8 -0
  107. package/plugins/runtime/document.ts +506 -0
  108. package/plugins/runtime/error.ts +154 -0
  109. package/plugins/runtime/heading-links.ts +31 -0
  110. package/plugins/runtime/index.ts +104 -0
  111. package/plugins/runtime/live.ts +51 -0
  112. package/plugins/runtime/navigation.ts +113 -0
  113. package/plugins/runtime/pending.ts +53 -0
  114. package/plugins/runtime/publication.ts +19 -0
  115. package/plugins/runtime/start.ts +177 -0
  116. package/plugins/runtime/static-client.ts +6 -0
  117. package/plugins/runtime/static-live.ts +188 -0
  118. package/plugins/runtime/view-transition.ts +72 -0
  119. package/plugins/runtime/viewport.ts +110 -0
  120. package/plugins/schema.ts +24 -0
  121. package/plugins/scripts.ts +140 -0
  122. package/plugins/source-preview/client.ts +256 -0
  123. package/plugins/source-preview/index.ts +132 -0
  124. package/plugins/source-preview/model.ts +11 -0
  125. package/plugins/source-preview/preview.css +107 -0
  126. package/plugins/styles.ts +145 -0
  127. package/plugins/theme/components.ts +10 -0
  128. package/plugins/theme/foundation.css +358 -0
  129. package/plugins/theme/index.ts +333 -0
  130. package/plugins/theme/prose.css +284 -0
  131. package/plugins/theme/scheme.js +63 -0
  132. package/plugins/theme/toggle.css +44 -0
  133. package/plugins/theme/view-transition.css +68 -0
  134. package/plugins/toc/client.ts +146 -0
  135. package/plugins/toc/index.ts +232 -0
  136. package/plugins/toc/theme.css +172 -0
  137. package/plugins/types.ts +538 -0
  138. package/problem.ts +41 -0
  139. package/project.ts +6 -0
  140. package/publish/build.ts +402 -0
  141. package/publish/concurrent.ts +32 -0
  142. package/publish/identity.ts +63 -0
  143. package/publish/index.ts +11 -0
  144. package/publish/location.ts +167 -0
  145. package/publish/model.ts +58 -0
  146. package/publish/plan.ts +300 -0
  147. package/publish/validate.ts +150 -0
  148. package/query/candidate-index.ts +101 -0
  149. package/query/dependency-index.ts +99 -0
  150. package/query/fields.ts +216 -0
  151. package/query/invalidation.ts +48 -0
  152. package/query/kernel.ts +776 -0
  153. package/query/model.ts +79 -0
  154. package/repository.ts +374 -0
  155. package/runtime-profile.ts +34 -0
  156. package/schema/model.ts +92 -0
  157. package/schema/registry.ts +642 -0
  158. package/schema/standard.ts +226 -0
  159. package/server/context.ts +94 -0
  160. package/server/css-imports.ts +66 -0
  161. package/server/http.ts +333 -0
  162. package/server/page.ts +21 -0
  163. package/server/protocols.ts +12 -0
  164. package/server/representations.ts +66 -0
  165. package/server/router.ts +131 -0
  166. package/server/routes.ts +202 -0
  167. package/server/serve.ts +5 -0
  168. package/server/watch.ts +120 -0
  169. package/source/blocks.ts +71 -0
  170. package/source/format.ts +164 -0
  171. package/source/index.ts +67 -0
  172. package/source/interpolate.ts +63 -0
  173. package/source/markdown/blocks.ts +46 -0
  174. package/source/markdown/comments.ts +32 -0
  175. package/source/markdown/fences.ts +67 -0
  176. package/source/markdown/headings.ts +88 -0
  177. package/source/markdown/index.ts +34 -0
  178. package/source/markdown/inline-code.ts +6 -0
  179. package/source/markdown/links.ts +326 -0
  180. package/source/markdown/lists.ts +223 -0
  181. package/source/markdown/prose.ts +74 -0
  182. package/source/markdown/scan.ts +79 -0
  183. package/source/markdown/text.ts +82 -0
  184. package/source/meta.ts +87 -0
  185. package/source/source.ts +121 -0
  186. package/source/topmatter.ts +57 -0
  187. package/source/utils.ts +21 -0
  188. package/source-diagnostic.ts +63 -0
  189. package/text-file.ts +26 -0
  190. package/util.ts +35 -0
  191. package/view/components.ts +499 -0
  192. package/view/model.ts +91 -0
  193. package/view/projection.ts +162 -0
  194. package/view/session.ts +146 -0
  195. package/view/syntax.ts +372 -0
@@ -0,0 +1,104 @@
1
+ import { matchesInvalidation } from "../../invalidation.ts"
2
+ import { publicPath } from "../../server/router.ts"
3
+ import { route, type Plugin, type ServerContext } from "../types.ts"
4
+
5
+ const clientEntry = new URL("./client.ts", import.meta.url)
6
+ const staticClientEntry = new URL("./static-client.ts", import.meta.url)
7
+ const LIVE_HEARTBEAT_INTERVAL = 30_000
8
+
9
+ function serveLive(
10
+ ctx: ServerContext,
11
+ req: Request,
12
+ route: string,
13
+ actor?: string,
14
+ privateResponse = false,
15
+ ) {
16
+ const encoder = new TextEncoder()
17
+ let unsubscribe: (() => void) | undefined
18
+ let heartbeat: ReturnType<typeof setInterval> | undefined
19
+ const cleanup = () => {
20
+ unsubscribe?.()
21
+ unsubscribe = undefined
22
+ if (heartbeat) clearInterval(heartbeat)
23
+ heartbeat = undefined
24
+ }
25
+ const stream = new ReadableStream({
26
+ start(controller) {
27
+ controller.enqueue(encoder.encode(": connected\n\n"))
28
+ heartbeat = setInterval(() => {
29
+ try {
30
+ controller.enqueue(encoder.encode(": keep-alive\n\n"))
31
+ } catch {
32
+ cleanup()
33
+ }
34
+ }, LIVE_HEARTBEAT_INTERVAL)
35
+ unsubscribe = ctx.onSync((event) => {
36
+ if (!matchesInvalidation(event, route, actor)) return
37
+ try {
38
+ // EventSource only dispatches events that contain a data field.
39
+ // Keep it opaque: routing already happened on the server.
40
+ controller.enqueue(encoder.encode("event: sync\ndata: changed\n\n"))
41
+ } catch {
42
+ cleanup()
43
+ }
44
+ })
45
+ req.signal.addEventListener(
46
+ "abort",
47
+ () => {
48
+ cleanup()
49
+ try {
50
+ controller.close()
51
+ } catch {
52
+ // already closed
53
+ }
54
+ },
55
+ { once: true },
56
+ )
57
+ },
58
+ cancel() {
59
+ cleanup()
60
+ },
61
+ })
62
+ const headers = new Headers({
63
+ "Content-Type": "text/event-stream",
64
+ "Cache-Control": privateResponse ? "private, no-store" : "no-cache",
65
+ Connection: "keep-alive",
66
+ })
67
+ if (privateResponse) headers.set("Vary", "Cookie")
68
+ return new Response(stream, { headers })
69
+ }
70
+
71
+ export function RuntimePlugin(): Plugin {
72
+ return {
73
+ id: "runtime",
74
+ name: "RuntimePlugin",
75
+ routes: [
76
+ route.get("live", async (ctx) => {
77
+ const { request, url } = ctx
78
+ const requested = url.searchParams.get("route")
79
+ const source = requested ? ctx.resolve(requested) : undefined
80
+ if (!source) {
81
+ return new Response("Unknown live route.", { status: 400 })
82
+ }
83
+ const session = await ctx.session()
84
+ const access = await ctx.readAccess(source)
85
+ if (access === "denied") {
86
+ return new Response("Not Found", { status: 404 })
87
+ }
88
+ return serveLive(
89
+ ctx,
90
+ request,
91
+ publicPath(source),
92
+ session.type === "authenticated" ? session.actor.id : undefined,
93
+ access === "private" || session.type === "authenticated",
94
+ )
95
+ }),
96
+ ],
97
+ async documentAssets(ctx, app) {
98
+ await ctx.addBrowserBundle(
99
+ app.runtime.target === "static" ? staticClientEntry : clientEntry,
100
+ "runtime",
101
+ )
102
+ },
103
+ }
104
+ }
@@ -0,0 +1,51 @@
1
+ export type LiveSubscription = {
2
+ bind(): void
3
+ close(): void
4
+ }
5
+
6
+ export type LiveSync = () => void | Promise<void>
7
+ export type LiveSubscriptionFactory = (onSync: LiveSync) => LiveSubscription
8
+
9
+ function liveTarget() {
10
+ const view = document.querySelector("[data-pmx-view]")
11
+ if (!(view instanceof HTMLElement)) return
12
+ const route = view.dataset.pmxRoute
13
+ if (!route) return
14
+ return { route }
15
+ }
16
+
17
+ export function createLiveSubscription(onSync: LiveSync): LiveSubscription {
18
+ let connection: { key: string; source: EventSource } | undefined
19
+
20
+ return {
21
+ bind() {
22
+ const target = liveTarget()
23
+ if (!target) return
24
+ const key = target.route
25
+ if (connection?.key === key) return
26
+
27
+ connection?.source.close()
28
+ const url = new URL("/api/runtime/live", location.href)
29
+ url.searchParams.set("route", target.route)
30
+ const source = new EventSource(url)
31
+ let interrupted = false
32
+ source.addEventListener("error", () => {
33
+ interrupted = true
34
+ })
35
+ source.addEventListener("open", () => {
36
+ if (!interrupted || connection?.source !== source) return
37
+ // Reconcile what may have changed while offline. The runtime refresh
38
+ // will request a reload only when browser assets are incompatible.
39
+ interrupted = false
40
+ void onSync()
41
+ })
42
+ source.addEventListener("sync", () => void onSync())
43
+ connection = { key, source }
44
+ },
45
+
46
+ close() {
47
+ connection?.source.close()
48
+ connection = undefined
49
+ },
50
+ }
51
+ }
@@ -0,0 +1,113 @@
1
+ import { reportRuntimeError } from "./error.ts"
2
+ import { navigationViewTransitionTypes } from "./view-transition.ts"
3
+ import { publicationContains, publicationPath } from "./publication.ts"
4
+
5
+ export type NavigationVisit = (
6
+ destination: URL,
7
+ options: {
8
+ type: "navigation" | "traverse"
9
+ from: URL
10
+ signal: AbortSignal
11
+ viewTransition: boolean
12
+ viewTransitionTypes: readonly string[]
13
+ },
14
+ ) => Promise<{ type: "rendered" } | { type: "native"; url: URL }>
15
+
16
+ type BrowserNavigateEvent = Event & {
17
+ canIntercept: boolean
18
+ destination: { url: string; sameDocument: boolean; index?: number }
19
+ downloadRequest: string | null
20
+ formData: FormData | null
21
+ hashChange: boolean
22
+ hasUAVisualTransition: boolean
23
+ navigationType: "push" | "reload" | "replace" | "traverse"
24
+ signal: AbortSignal
25
+ sourceElement: Element | null
26
+ intercept(options: { handler(): Promise<void> }): void
27
+ }
28
+
29
+ type BrowserNavigation = EventTarget & {
30
+ currentEntry?: { index: number }
31
+ addEventListener(
32
+ type: "navigate",
33
+ listener: (event: BrowserNavigateEvent) => void,
34
+ ): void
35
+ removeEventListener(
36
+ type: "navigate",
37
+ listener: (event: BrowserNavigateEvent) => void,
38
+ ): void
39
+ }
40
+
41
+ function navigationApi() {
42
+ return (window as Window & { navigation?: BrowserNavigation }).navigation
43
+ }
44
+
45
+ function shouldIntercept(event: BrowserNavigateEvent) {
46
+ if (!event.canIntercept || event.hashChange) return false
47
+ if (event.destination.sameDocument && event.navigationType !== "traverse") {
48
+ return false
49
+ }
50
+ if (event.downloadRequest !== null || event.formData !== null) return false
51
+ if (event.navigationType === "reload") return false
52
+
53
+ const destination = new URL(event.destination.url)
54
+ if (destination.origin !== location.origin) return false
55
+ if (!publicationContains(destination.pathname)) return false
56
+ if (destination.pathname.startsWith(publicationPath("/.pmx/"))) return false
57
+ if (destination.pathname.startsWith(publicationPath("/actions/")))
58
+ return false
59
+ if (destination.pathname.endsWith(".md")) return false
60
+ if (event.sourceElement?.closest("[data-pmx-reload], [rel~='external']")) {
61
+ return false
62
+ }
63
+ return true
64
+ }
65
+
66
+ export function installNavigation(visit: NavigationVisit) {
67
+ const browserNavigation = navigationApi()
68
+ if (!browserNavigation) return
69
+
70
+ const navigate = (event: BrowserNavigateEvent) => {
71
+ if (!shouldIntercept(event)) return
72
+ const from = new URL(location.href)
73
+ const type = event.navigationType === "traverse" ? "traverse" : "navigation"
74
+ // Indexes are only reliable here. After intercept, currentEntry is the destination.
75
+ const viewTransitionTypes = navigationViewTransitionTypes({
76
+ type,
77
+ currentIndex: browserNavigation.currentEntry?.index,
78
+ destinationIndex: event.destination.index,
79
+ })
80
+ event.intercept({
81
+ async handler() {
82
+ try {
83
+ const result = await visit(new URL(event.destination.url), {
84
+ type,
85
+ from,
86
+ signal: event.signal,
87
+ viewTransition: !event.hasUAVisualTransition,
88
+ viewTransitionTypes,
89
+ })
90
+ if (result.type === "rendered" || event.signal.aborted) return
91
+ browserNavigation.removeEventListener("navigate", navigate)
92
+ location.replace(result.url)
93
+ } catch (error) {
94
+ if (event.signal.aborted) return
95
+ reportRuntimeError({
96
+ type,
97
+ outcome: "native",
98
+ message:
99
+ "The enhanced navigation failed. Loading the destination normally.",
100
+ })
101
+ console.error(
102
+ "PathMX navigation failed; loading the document.",
103
+ error,
104
+ )
105
+ browserNavigation.removeEventListener("navigate", navigate)
106
+ location.reload()
107
+ }
108
+ },
109
+ })
110
+ }
111
+
112
+ browserNavigation.addEventListener("navigate", navigate)
113
+ }
@@ -0,0 +1,53 @@
1
+ import type { RenderType } from "./document.ts"
2
+
3
+ export type PendingTarget = {
4
+ form: HTMLFormElement
5
+ submitter: HTMLElement | null
6
+ }
7
+
8
+ export type PendingDetail = {
9
+ pending: boolean
10
+ type: RenderType
11
+ }
12
+
13
+ function dispatchPending(detail: PendingDetail) {
14
+ document.dispatchEvent(
15
+ new CustomEvent<PendingDetail>("pmx:pending", { detail }),
16
+ )
17
+ }
18
+
19
+ function markViewBusy(busy: boolean) {
20
+ const view = document.querySelector("[data-pmx-view]")
21
+ if (!(view instanceof HTMLElement)) return
22
+ if (busy) view.setAttribute("aria-busy", "true")
23
+ else view.removeAttribute("aria-busy")
24
+ }
25
+
26
+ export function startPending(type: RenderType, target?: PendingTarget) {
27
+ const root = document.documentElement
28
+ const previousSubmitterState =
29
+ target?.submitter?.getAttribute("aria-disabled")
30
+
31
+ root.dataset.pmxPending = type
32
+ markViewBusy(true)
33
+ target?.form.setAttribute("data-pmx-pending", "")
34
+ target?.submitter?.setAttribute("data-pmx-pending", "")
35
+ target?.submitter?.setAttribute("aria-disabled", "true")
36
+ dispatchPending({ pending: true, type })
37
+
38
+ let finished = false
39
+ return () => {
40
+ if (finished) return
41
+ finished = true
42
+ if (root.dataset.pmxPending === type) delete root.dataset.pmxPending
43
+ markViewBusy(false)
44
+ target?.form.removeAttribute("data-pmx-pending")
45
+ target?.submitter?.removeAttribute("data-pmx-pending")
46
+ if (previousSubmitterState == null) {
47
+ target?.submitter?.removeAttribute("aria-disabled")
48
+ } else {
49
+ target?.submitter?.setAttribute("aria-disabled", previousSubmitterState)
50
+ }
51
+ dispatchPending({ pending: false, type })
52
+ }
53
+ }
@@ -0,0 +1,19 @@
1
+ export function publicationBasePath() {
2
+ const value = document.documentElement.dataset.pmxBase
3
+ return value?.startsWith("/") ? value : "/"
4
+ }
5
+
6
+ export function publicationPath(pathname: string) {
7
+ const basePath = publicationBasePath()
8
+ if (basePath === "/") return pathname
9
+ return pathname === "/" ? basePath : `${basePath}${pathname}`
10
+ }
11
+
12
+ export function publicationContains(pathname: string) {
13
+ const basePath = publicationBasePath()
14
+ return (
15
+ basePath === "/" ||
16
+ pathname === basePath ||
17
+ pathname.startsWith(`${basePath}/`)
18
+ )
19
+ }
@@ -0,0 +1,177 @@
1
+ import {
2
+ fetchPathMXDocument,
3
+ PathMXReloadRequiredError,
4
+ readPathMXDocument,
5
+ reconcilePathMXDocument,
6
+ type RenderType,
7
+ } from "./document.ts"
8
+ import type { LiveSubscriptionFactory } from "./live.ts"
9
+ import { installNavigation, type NavigationVisit } from "./navigation.ts"
10
+ import { installViewportChrome } from "./viewport.ts"
11
+ import { startPending, type PendingTarget } from "./pending.ts"
12
+ import { reportRuntimeError } from "./error.ts"
13
+ import { installHeadingLinks } from "./heading-links.ts"
14
+
15
+ type ActiveLoad = {
16
+ controller: AbortController
17
+ signal: AbortSignal
18
+ type: RenderType
19
+ finishPending: () => void
20
+ }
21
+
22
+ type RuntimeOptions = Readonly<{
23
+ actions?: RuntimeActionInstaller
24
+ live: LiveSubscriptionFactory
25
+ }>
26
+
27
+ type RuntimeActionLoad = Readonly<{
28
+ signal: AbortSignal
29
+ apply(response: Response, from: URL): Promise<void>
30
+ finish(): void
31
+ }>
32
+
33
+ export type RuntimeActionInstaller = (runtime: {
34
+ isNavigating(): boolean
35
+ begin(target: PendingTarget): RuntimeActionLoad | undefined
36
+ }) => void
37
+
38
+ export function startRuntime(options: RuntimeOptions) {
39
+ let active: ActiveLoad | undefined
40
+ let liveQueued = false
41
+
42
+ function begin(
43
+ type: RenderType,
44
+ signal?: AbortSignal,
45
+ target?: PendingTarget,
46
+ ) {
47
+ active?.finishPending()
48
+ active?.controller.abort()
49
+ const controller = new AbortController()
50
+ const load: ActiveLoad = {
51
+ controller,
52
+ signal: signal
53
+ ? AbortSignal.any([controller.signal, signal])
54
+ : controller.signal,
55
+ type,
56
+ finishPending: startPending(type, target),
57
+ }
58
+ active = load
59
+ return load
60
+ }
61
+
62
+ function finish(load: ActiveLoad) {
63
+ if (active !== load) return
64
+ active = undefined
65
+ load.finishPending()
66
+ if (liveQueued) queueMicrotask(runLiveRefresh)
67
+ }
68
+
69
+ function isCurrent(load: ActiveLoad) {
70
+ return active === load && !load.signal.aborted
71
+ }
72
+
73
+ function isNavigating() {
74
+ return active?.type === "navigation" || active?.type === "traverse"
75
+ }
76
+
77
+ function sameRequestUrl(left: URL, right: URL) {
78
+ return (
79
+ left.origin === right.origin &&
80
+ left.pathname === right.pathname &&
81
+ left.search === right.search
82
+ )
83
+ }
84
+
85
+ async function apply(
86
+ load: ActiveLoad,
87
+ loaded: Awaited<ReturnType<typeof fetchPathMXDocument>>,
88
+ settings: {
89
+ from: URL
90
+ viewTransition: boolean
91
+ viewTransitionTypes?: readonly string[]
92
+ },
93
+ ) {
94
+ if (!isCurrent(load)) return
95
+ await reconcilePathMXDocument(loaded, {
96
+ type: load.type,
97
+ from: settings.from,
98
+ viewTransition: settings.viewTransition,
99
+ viewTransitionTypes: settings.viewTransitionTypes ?? [],
100
+ })
101
+ live.bind()
102
+ }
103
+
104
+ const visit: NavigationVisit = async (destination, settings) => {
105
+ const load = begin(settings.type, settings.signal)
106
+ try {
107
+ const loaded = await fetchPathMXDocument(destination, load.signal)
108
+ if (!sameRequestUrl(loaded.responseUrl, destination)) {
109
+ return { type: "native", url: loaded.responseUrl }
110
+ }
111
+ await apply(load, loaded, settings)
112
+ return { type: "rendered" }
113
+ } finally {
114
+ finish(load)
115
+ }
116
+ }
117
+
118
+ function refreshLive() {
119
+ liveQueued = true
120
+ if (!active) queueMicrotask(runLiveRefresh)
121
+ }
122
+
123
+ async function runLiveRefresh() {
124
+ if (active || !liveQueued) return
125
+ liveQueued = false
126
+ const load = begin("live")
127
+ const url = new URL(location.href)
128
+ try {
129
+ const loaded = await fetchPathMXDocument(url, load.signal, "no-store")
130
+ await apply(load, loaded, { from: url, viewTransition: false })
131
+ } catch (error) {
132
+ if (load.signal.aborted) return
133
+ if (error instanceof PathMXReloadRequiredError) {
134
+ liveQueued = false
135
+ location.reload()
136
+ return
137
+ }
138
+ reportRuntimeError({
139
+ type: "live",
140
+ outcome: "stale",
141
+ message:
142
+ "The live refresh failed. The current page remains available and may be stale.",
143
+ })
144
+ console.error("PathMX live refresh failed.", error)
145
+ } finally {
146
+ finish(load)
147
+ }
148
+ }
149
+
150
+ const live = options.live(refreshLive)
151
+
152
+ options.actions?.({
153
+ isNavigating,
154
+ begin(target) {
155
+ if (active?.type === "action") return
156
+ const load = begin("action", undefined, target)
157
+ return {
158
+ signal: load.signal,
159
+ async apply(response, from) {
160
+ if (!sameRequestUrl(from, new URL(location.href))) return
161
+ const loaded = await readPathMXDocument(response, from)
162
+ await apply(load, loaded, { from, viewTransition: false })
163
+ },
164
+ finish() {
165
+ finish(load)
166
+ },
167
+ }
168
+ },
169
+ })
170
+
171
+ live.bind()
172
+ installHeadingLinks()
173
+ installViewportChrome()
174
+ installNavigation(visit)
175
+ addEventListener("pagehide", () => live.close())
176
+ addEventListener("pageshow", () => live.bind())
177
+ }
@@ -0,0 +1,6 @@
1
+ import { createPollingLiveSubscription } from "./static-live.ts"
2
+ import { startRuntime } from "./start.ts"
3
+
4
+ startRuntime({
5
+ live: createPollingLiveSubscription,
6
+ })