@hitslop/runtime 0.1.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.
- package/LICENSE +21 -0
- package/README.md +20 -0
- package/dist/adapter.d.ts +6 -0
- package/dist/adapter.js +6 -0
- package/dist/index.d.ts +39 -0
- package/dist/index.js +71 -0
- package/dist/json-persister.d.ts +48 -0
- package/dist/json-persister.js +164 -0
- package/dist/media-picker.d.ts +7 -0
- package/dist/media-picker.js +40 -0
- package/dist/sql.d.ts +3 -0
- package/dist/sql.js +10 -0
- package/dist/types.d.ts +79 -0
- package/dist/types.js +0 -0
- package/package.json +33 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 hitSlop contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# @hitslop/runtime
|
|
2
|
+
|
|
3
|
+
The framework-neutral browser bridge for hitSlop documents.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
import { capture, ready, slop, sql } from "@hitslop/runtime";
|
|
7
|
+
|
|
8
|
+
const snapshot = await slop.json.open({ count: 0 });
|
|
9
|
+
await slop.db.execute(...sql`create table if not exists notes (body text)`);
|
|
10
|
+
await slop.window.resize({ width: 560, height: 480 });
|
|
11
|
+
ready();
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Use `@hitslop/runtime/adapter` only when implementing a framework adapter; it
|
|
15
|
+
exports the shared JSON persister and safe media helpers.
|
|
16
|
+
|
|
17
|
+
Documentation: [Architecture](https://github.com/hitslop/hitslop/blob/main/docs/architecture.md) ·
|
|
18
|
+
[Storage](https://github.com/hitslop/hitslop/blob/main/docs/storage.md)
|
|
19
|
+
|
|
20
|
+
MIT © 2026 hitSlop contributors.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building blocks for framework adapters. Ordinary slop applications should
|
|
3
|
+
* import `slop`, `ready`, `capture`, and `sql` from `@hitslop/runtime` instead.
|
|
4
|
+
*/
|
|
5
|
+
export { JsonPersister, type JsonSnapshot } from "./json-persister.js";
|
|
6
|
+
export { chooseLocalFile, fileToBase64, mediaSourceURL, safeMediaName } from "./media-picker.js";
|
package/dist/adapter.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Building blocks for framework adapters. Ordinary slop applications should
|
|
3
|
+
* import `slop`, `ready`, `capture`, and `sql` from `@hitslop/runtime` instead.
|
|
4
|
+
*/
|
|
5
|
+
export { JsonPersister } from "./json-persister.js";
|
|
6
|
+
export { chooseLocalFile, fileToBase64, mediaSourceURL, safeMediaName } from "./media-picker.js";
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { SlopHost, SlopStatement, SlopWindowSize } from "./types.ts";
|
|
2
|
+
export type { SlopChange, SlopHost, SlopMediaSnapshot, SlopSnapshot, SlopStatement, SlopStoreKind, SlopWindowSize, WindowSlop } from "./types.ts";
|
|
3
|
+
export { sql } from "./sql.js";
|
|
4
|
+
export declare function installHost(host: SlopHost): () => void;
|
|
5
|
+
export declare function getHost(): SlopHost;
|
|
6
|
+
export declare const slop: {
|
|
7
|
+
json: {
|
|
8
|
+
open: <T>(initialValue: T) => Promise<import("./types.ts").SlopSnapshot<T>>;
|
|
9
|
+
read: <T>() => Promise<import("./types.ts").SlopSnapshot<T>>;
|
|
10
|
+
write: <T>(value: T, expectedRevision?: string) => Promise<{
|
|
11
|
+
revision: string;
|
|
12
|
+
}>;
|
|
13
|
+
onChange: (callback: Parameters<SlopHost["watch"]>[1]) => () => void;
|
|
14
|
+
};
|
|
15
|
+
db: {
|
|
16
|
+
query: <T = Record<string, unknown>>(sql: string, params?: unknown[]) => Promise<T[]>;
|
|
17
|
+
execute: (sql: string, params?: unknown[]) => Promise<number>;
|
|
18
|
+
transaction: (statements: SlopStatement[]) => Promise<number>;
|
|
19
|
+
onChange: (callback: Parameters<SlopHost["watch"]>[1]) => () => void;
|
|
20
|
+
};
|
|
21
|
+
media: {
|
|
22
|
+
open: (name: string) => Promise<import("./types.ts").SlopMediaSnapshot>;
|
|
23
|
+
write: (name: string, data: string, mimeType: string) => Promise<{
|
|
24
|
+
revision: string;
|
|
25
|
+
}>;
|
|
26
|
+
remove: (name: string) => Promise<{
|
|
27
|
+
revision: null;
|
|
28
|
+
}>;
|
|
29
|
+
onChange: (callback: Parameters<SlopHost["watch"]>[1]) => () => void;
|
|
30
|
+
};
|
|
31
|
+
window: {
|
|
32
|
+
resize: (size: SlopWindowSize) => Promise<SlopWindowSize>;
|
|
33
|
+
drag: () => Promise<void>;
|
|
34
|
+
};
|
|
35
|
+
};
|
|
36
|
+
export declare function ready(): void;
|
|
37
|
+
export declare const capture: {
|
|
38
|
+
isRenderer: () => boolean;
|
|
39
|
+
};
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export { sql } from "./sql.js";
|
|
2
|
+
let configuredHost;
|
|
3
|
+
const fromBridge = (bridge) => ({
|
|
4
|
+
query: (sql, params = []) => bridge.db.query(sql, params),
|
|
5
|
+
execute: (sql, params = []) => bridge.db.execute(sql, params),
|
|
6
|
+
transaction: (statements) => bridge.db.transaction(statements),
|
|
7
|
+
jsonOpen: async (initialValue) => {
|
|
8
|
+
const result = await bridge.json.open(initialValue);
|
|
9
|
+
return { value: result.value, revision: result.revision };
|
|
10
|
+
},
|
|
11
|
+
jsonRead: async () => {
|
|
12
|
+
const result = await bridge.json.read();
|
|
13
|
+
return { value: result.value, revision: result.revision };
|
|
14
|
+
},
|
|
15
|
+
jsonWrite: async (value, expectedRevision) => bridge.json.write(value, expectedRevision),
|
|
16
|
+
mediaOpen: (name) => bridge.media.open(name),
|
|
17
|
+
mediaWrite: (name, data, mimeType) => bridge.media.write(name, data, mimeType),
|
|
18
|
+
mediaRemove: (name) => bridge.media.remove(name),
|
|
19
|
+
resizeWindow: (size) => bridge.window?.resize
|
|
20
|
+
? bridge.window.resize(size)
|
|
21
|
+
: Promise.reject(new Error("The hitSlop host does not support dynamic window sizing.")),
|
|
22
|
+
dragWindow: () => bridge.window?.drag
|
|
23
|
+
? bridge.window.drag()
|
|
24
|
+
: Promise.reject(new Error("The hitSlop host does not support window dragging.")),
|
|
25
|
+
watch: (kind, callback) => kind === "json" ? bridge.json.onChange(callback) : kind === "sqlite" ? bridge.db.onChange(callback) : bridge.media.onChange(callback),
|
|
26
|
+
});
|
|
27
|
+
export function installHost(host) {
|
|
28
|
+
configuredHost = host;
|
|
29
|
+
return () => { if (configuredHost === host)
|
|
30
|
+
configuredHost = undefined; };
|
|
31
|
+
}
|
|
32
|
+
export function getHost() {
|
|
33
|
+
if (configuredHost)
|
|
34
|
+
return configuredHost;
|
|
35
|
+
if (typeof window !== "undefined" && window.slop?.json && window.slop.db && window.slop.media)
|
|
36
|
+
return fromBridge(window.slop);
|
|
37
|
+
throw new Error("hitSlop host bridge is unavailable. Run this project with `slop dev` or inside hitSlop.");
|
|
38
|
+
}
|
|
39
|
+
export const slop = {
|
|
40
|
+
json: {
|
|
41
|
+
open: (initialValue) => getHost().jsonOpen(initialValue),
|
|
42
|
+
read: () => getHost().jsonRead(),
|
|
43
|
+
write: (value, expectedRevision) => getHost().jsonWrite(value, expectedRevision),
|
|
44
|
+
onChange: (callback) => getHost().watch("json", callback),
|
|
45
|
+
},
|
|
46
|
+
db: {
|
|
47
|
+
query: (sql, params) => getHost().query(sql, params),
|
|
48
|
+
execute: (sql, params) => getHost().execute(sql, params),
|
|
49
|
+
transaction: (statements) => getHost().transaction(statements),
|
|
50
|
+
onChange: (callback) => getHost().watch("sqlite", callback),
|
|
51
|
+
},
|
|
52
|
+
media: {
|
|
53
|
+
open: (name) => getHost().mediaOpen(name),
|
|
54
|
+
write: (name, data, mimeType) => getHost().mediaWrite(name, data, mimeType),
|
|
55
|
+
remove: (name) => getHost().mediaRemove(name),
|
|
56
|
+
onChange: (callback) => getHost().watch("media", callback),
|
|
57
|
+
},
|
|
58
|
+
window: {
|
|
59
|
+
resize: (size) => getHost().resizeWindow(size),
|
|
60
|
+
drag: () => getHost().dragWindow(),
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
export function ready() { if (typeof window !== "undefined")
|
|
64
|
+
window.slop?.ready?.(); }
|
|
65
|
+
/// Icon capture support. The native host renders document assets in a
|
|
66
|
+
/// hidden WebView with `<html data-slop-renderer="true">` set before any guest
|
|
67
|
+
/// code runs; use `capture.isRenderer()` to mount the `data-slop-render`
|
|
68
|
+
/// icon target only in that pass and keep it out of the interactive app.
|
|
69
|
+
export const capture = {
|
|
70
|
+
isRenderer: () => typeof document !== "undefined" && document.documentElement.dataset.slopRenderer === "true",
|
|
71
|
+
};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
export type JsonSnapshot<T> = {
|
|
2
|
+
json: string;
|
|
3
|
+
value: T;
|
|
4
|
+
};
|
|
5
|
+
type Source = "package" | "app" | "external";
|
|
6
|
+
type Options<T> = {
|
|
7
|
+
fallback: JsonSnapshot<T>;
|
|
8
|
+
io: {
|
|
9
|
+
open: (initialValue: T) => Promise<{
|
|
10
|
+
value: T;
|
|
11
|
+
revision: string;
|
|
12
|
+
}>;
|
|
13
|
+
read: () => Promise<{
|
|
14
|
+
value: T;
|
|
15
|
+
revision: string;
|
|
16
|
+
}>;
|
|
17
|
+
write: (value: T, expectedRevision: string) => Promise<{
|
|
18
|
+
revision: string;
|
|
19
|
+
}>;
|
|
20
|
+
};
|
|
21
|
+
getLocal: () => JsonSnapshot<T>;
|
|
22
|
+
onAdopt: (value: T, source: Source) => void;
|
|
23
|
+
onRevision: (revision: string | null) => void;
|
|
24
|
+
onSource: (source: Source) => void;
|
|
25
|
+
onError: (message: string | null) => void;
|
|
26
|
+
};
|
|
27
|
+
export declare class JsonPersister<T> {
|
|
28
|
+
private options;
|
|
29
|
+
private revision;
|
|
30
|
+
private lastPersistedJson;
|
|
31
|
+
private pending;
|
|
32
|
+
private loaded;
|
|
33
|
+
private writing;
|
|
34
|
+
private stopped;
|
|
35
|
+
private localVersion;
|
|
36
|
+
private draining;
|
|
37
|
+
private operations;
|
|
38
|
+
constructor(options: Options<T>);
|
|
39
|
+
localChanged(json: string, value: T): void;
|
|
40
|
+
reload(): Promise<void>;
|
|
41
|
+
externalChanged(eventRevision?: string | null): void;
|
|
42
|
+
private requestDrain;
|
|
43
|
+
private drainLoop;
|
|
44
|
+
private adopt;
|
|
45
|
+
private schedule;
|
|
46
|
+
private message;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
export class JsonPersister {
|
|
2
|
+
options;
|
|
3
|
+
revision = null;
|
|
4
|
+
lastPersistedJson;
|
|
5
|
+
pending = null;
|
|
6
|
+
loaded = false;
|
|
7
|
+
writing = false;
|
|
8
|
+
stopped = false;
|
|
9
|
+
localVersion = 0;
|
|
10
|
+
draining = null;
|
|
11
|
+
operations = Promise.resolve(undefined);
|
|
12
|
+
constructor(options) {
|
|
13
|
+
this.options = options;
|
|
14
|
+
this.lastPersistedJson = options.fallback.json;
|
|
15
|
+
}
|
|
16
|
+
localChanged(json, value) {
|
|
17
|
+
this.localVersion += 1;
|
|
18
|
+
// A write already in flight may change what is persisted, so a reversion to
|
|
19
|
+
// the previously persisted value still has to remain queued until it lands.
|
|
20
|
+
if (!this.writing && json === this.lastPersistedJson) {
|
|
21
|
+
this.pending = null;
|
|
22
|
+
this.stopped = false;
|
|
23
|
+
this.options.onError(null);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
this.pending = { json, value };
|
|
27
|
+
this.stopped = false;
|
|
28
|
+
this.requestDrain();
|
|
29
|
+
}
|
|
30
|
+
reload() {
|
|
31
|
+
const wasLoaded = this.loaded;
|
|
32
|
+
const requestedAtVersion = this.localVersion;
|
|
33
|
+
// Once loaded, reload is an explicit escape hatch: discard edits that were
|
|
34
|
+
// pending when it was requested. Mutations made after this call still win.
|
|
35
|
+
if (wasLoaded) {
|
|
36
|
+
this.pending = null;
|
|
37
|
+
this.stopped = false;
|
|
38
|
+
}
|
|
39
|
+
return this.schedule(async () => {
|
|
40
|
+
try {
|
|
41
|
+
const result = wasLoaded
|
|
42
|
+
? await this.options.io.read()
|
|
43
|
+
: await this.options.io.open(this.options.fallback.value);
|
|
44
|
+
const persistedJson = JSON.stringify(result.value);
|
|
45
|
+
this.loaded = true;
|
|
46
|
+
this.revision = result.revision;
|
|
47
|
+
this.lastPersistedJson = persistedJson;
|
|
48
|
+
this.options.onRevision(result.revision);
|
|
49
|
+
const local = this.options.getLocal();
|
|
50
|
+
const changedDuringReload = this.localVersion !== requestedAtVersion;
|
|
51
|
+
const localWins = wasLoaded
|
|
52
|
+
? changedDuringReload && local.json !== persistedJson
|
|
53
|
+
: local.json !== this.options.fallback.json;
|
|
54
|
+
if (localWins) {
|
|
55
|
+
this.pending = local;
|
|
56
|
+
this.requestDrain();
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
this.pending = null;
|
|
60
|
+
this.adopt(result.value, result.revision, wasLoaded ? "external" : "package");
|
|
61
|
+
}
|
|
62
|
+
this.options.onError(null);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
const message = this.message(error);
|
|
66
|
+
this.options.onError(message);
|
|
67
|
+
throw error;
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
externalChanged(eventRevision) {
|
|
72
|
+
if (eventRevision && eventRevision === this.revision)
|
|
73
|
+
return;
|
|
74
|
+
if (!this.loaded || this.writing || this.pending || this.stopped)
|
|
75
|
+
return;
|
|
76
|
+
const requestedAtVersion = this.localVersion;
|
|
77
|
+
void this.schedule(async () => {
|
|
78
|
+
if (this.writing || this.pending || this.stopped)
|
|
79
|
+
return;
|
|
80
|
+
try {
|
|
81
|
+
const result = await this.options.io.read();
|
|
82
|
+
if (this.writing || this.pending || this.stopped || this.localVersion !== requestedAtVersion)
|
|
83
|
+
return;
|
|
84
|
+
this.adopt(result.value, result.revision, "external");
|
|
85
|
+
this.options.onError(null);
|
|
86
|
+
}
|
|
87
|
+
catch (error) {
|
|
88
|
+
this.options.onError(this.message(error));
|
|
89
|
+
}
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
requestDrain() {
|
|
93
|
+
if (this.draining || !this.loaded || this.stopped || !this.pending)
|
|
94
|
+
return;
|
|
95
|
+
this.draining = this.schedule(() => this.drainLoop()).finally(() => {
|
|
96
|
+
this.draining = null;
|
|
97
|
+
if (this.loaded && this.pending && !this.stopped)
|
|
98
|
+
this.requestDrain();
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
async drainLoop() {
|
|
102
|
+
if (!this.loaded || this.stopped)
|
|
103
|
+
return;
|
|
104
|
+
this.writing = true;
|
|
105
|
+
try {
|
|
106
|
+
while (this.pending && !this.stopped) {
|
|
107
|
+
const snapshot = this.pending;
|
|
108
|
+
this.pending = null;
|
|
109
|
+
if (snapshot.json === this.lastPersistedJson) {
|
|
110
|
+
this.options.onError(null);
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
const result = await this.options.io.write(snapshot.value, this.revision ?? "");
|
|
115
|
+
this.revision = result.revision;
|
|
116
|
+
this.lastPersistedJson = snapshot.json;
|
|
117
|
+
this.options.onRevision(result.revision);
|
|
118
|
+
this.options.onSource("app");
|
|
119
|
+
this.options.onError(null);
|
|
120
|
+
}
|
|
121
|
+
catch (error) {
|
|
122
|
+
if (this.message(error).includes("revision_conflict")) {
|
|
123
|
+
try {
|
|
124
|
+
const result = await this.options.io.read();
|
|
125
|
+
this.revision = result.revision;
|
|
126
|
+
this.lastPersistedJson = JSON.stringify(result.value);
|
|
127
|
+
this.options.onRevision(result.revision);
|
|
128
|
+
this.pending ??= snapshot;
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
catch (readError) {
|
|
132
|
+
this.pending ??= snapshot;
|
|
133
|
+
this.stopped = true;
|
|
134
|
+
this.options.onError(this.message(readError));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
// Keep the newest full snapshot dirty. A later mutation replaces it
|
|
139
|
+
// and re-arms persistence without losing any fields.
|
|
140
|
+
this.pending ??= snapshot;
|
|
141
|
+
this.stopped = true;
|
|
142
|
+
this.options.onError(this.message(error));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
finally {
|
|
147
|
+
this.writing = false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
adopt(value, revision, source) {
|
|
151
|
+
this.revision = revision;
|
|
152
|
+
this.lastPersistedJson = JSON.stringify(value);
|
|
153
|
+
this.options.onRevision(revision);
|
|
154
|
+
this.options.onAdopt(value, source);
|
|
155
|
+
}
|
|
156
|
+
schedule(operation) {
|
|
157
|
+
const scheduled = this.operations.then(operation, operation);
|
|
158
|
+
this.operations = scheduled.catch(() => undefined);
|
|
159
|
+
return scheduled;
|
|
160
|
+
}
|
|
161
|
+
message(error) {
|
|
162
|
+
return error instanceof Error ? error.message : String(error);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export declare const safeMediaName: (name: string, label?: string) => string;
|
|
2
|
+
/** Reads a File into the base64 payload expected by `slop.media.write`. */
|
|
3
|
+
export declare const fileToBase64: (file: File, label?: string) => Promise<string>;
|
|
4
|
+
/** Opens the native file picker via a hidden `<input type="file">`. */
|
|
5
|
+
export declare const chooseLocalFile: (accept: string, onFile: (file: File) => void) => void;
|
|
6
|
+
/** URL for a named media entry, cache-busted by its revision. */
|
|
7
|
+
export declare const mediaSourceURL: (name: string, revision: string | null) => string;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// Framework-agnostic helpers shared by the media store adapters
|
|
2
|
+
// (@hitslop/svelte, @hitslop/react). No reactivity in here: adapters own state.
|
|
3
|
+
export const safeMediaName = (name, label = "Media") => {
|
|
4
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(name))
|
|
5
|
+
throw new Error(`${label} store names must use lowercase letters, numbers, and hyphens`);
|
|
6
|
+
return name;
|
|
7
|
+
};
|
|
8
|
+
/** Reads a File into the base64 payload expected by `slop.media.write`. */
|
|
9
|
+
export const fileToBase64 = async (file, label = "file") => {
|
|
10
|
+
let bytes;
|
|
11
|
+
try {
|
|
12
|
+
bytes = new Uint8Array(await file.arrayBuffer());
|
|
13
|
+
}
|
|
14
|
+
catch {
|
|
15
|
+
throw new Error(`Could not read that ${label}`);
|
|
16
|
+
}
|
|
17
|
+
let binary = "";
|
|
18
|
+
const chunkSize = 0x8000; // Keep String.fromCharCode argument counts bounded.
|
|
19
|
+
for (let offset = 0; offset < bytes.length; offset += chunkSize) {
|
|
20
|
+
binary += String.fromCharCode(...bytes.subarray(offset, offset + chunkSize));
|
|
21
|
+
}
|
|
22
|
+
return btoa(binary);
|
|
23
|
+
};
|
|
24
|
+
/** Opens the native file picker via a hidden `<input type="file">`. */
|
|
25
|
+
export const chooseLocalFile = (accept, onFile) => {
|
|
26
|
+
const input = document.createElement("input");
|
|
27
|
+
input.type = "file";
|
|
28
|
+
input.accept = accept;
|
|
29
|
+
input.hidden = true;
|
|
30
|
+
input.addEventListener("change", () => {
|
|
31
|
+
const file = input.files?.[0];
|
|
32
|
+
input.remove();
|
|
33
|
+
if (file)
|
|
34
|
+
onFile(file);
|
|
35
|
+
}, { once: true });
|
|
36
|
+
document.body.append(input);
|
|
37
|
+
input.click();
|
|
38
|
+
};
|
|
39
|
+
/** URL for a named media entry, cache-busted by its revision. */
|
|
40
|
+
export const mediaSourceURL = (name, revision) => `/media/${name}?revision=${encodeURIComponent(revision ?? "current")}`;
|
package/dist/sql.d.ts
ADDED
package/dist/sql.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Tagged template that turns interpolated values into positional `?` parameters. */
|
|
2
|
+
export function sql(strings, ...values) {
|
|
3
|
+
let text = strings[0] ?? "";
|
|
4
|
+
const parameters = [];
|
|
5
|
+
values.forEach((value, index) => {
|
|
6
|
+
text += `?${strings[index + 1] ?? ""}`;
|
|
7
|
+
parameters.push(value);
|
|
8
|
+
});
|
|
9
|
+
return { sql: text, parameters };
|
|
10
|
+
}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
export type SlopStoreKind = "json" | "sqlite" | "media";
|
|
2
|
+
export type SlopChange = {
|
|
3
|
+
kind: SlopStoreKind;
|
|
4
|
+
source: "app" | "external" | "dev";
|
|
5
|
+
revision?: string | null;
|
|
6
|
+
sequence?: number;
|
|
7
|
+
};
|
|
8
|
+
export type SlopStatement = {
|
|
9
|
+
sql: string;
|
|
10
|
+
parameters?: unknown[];
|
|
11
|
+
};
|
|
12
|
+
export type SlopSnapshot<T> = {
|
|
13
|
+
value: T;
|
|
14
|
+
revision: string;
|
|
15
|
+
};
|
|
16
|
+
export type SlopMediaSnapshot = {
|
|
17
|
+
exists: boolean;
|
|
18
|
+
revision: string | null;
|
|
19
|
+
};
|
|
20
|
+
export type SlopWindowSize = {
|
|
21
|
+
width: number;
|
|
22
|
+
height: number;
|
|
23
|
+
};
|
|
24
|
+
export interface SlopHost {
|
|
25
|
+
query<T = Record<string, unknown>>(sql: string, params?: unknown[]): Promise<T[]>;
|
|
26
|
+
execute(sql: string, params?: unknown[]): Promise<number>;
|
|
27
|
+
transaction(statements: SlopStatement[]): Promise<number>;
|
|
28
|
+
jsonOpen<T>(initialValue: T): Promise<SlopSnapshot<T>>;
|
|
29
|
+
jsonRead<T>(): Promise<SlopSnapshot<T>>;
|
|
30
|
+
jsonWrite<T>(value: T, expectedRevision?: string): Promise<{
|
|
31
|
+
revision: string;
|
|
32
|
+
}>;
|
|
33
|
+
mediaOpen(name: string): Promise<SlopMediaSnapshot>;
|
|
34
|
+
mediaWrite(name: string, data: string, mimeType: string): Promise<{
|
|
35
|
+
revision: string;
|
|
36
|
+
}>;
|
|
37
|
+
mediaRemove(name: string): Promise<{
|
|
38
|
+
revision: null;
|
|
39
|
+
}>;
|
|
40
|
+
resizeWindow(size: SlopWindowSize): Promise<SlopWindowSize>;
|
|
41
|
+
dragWindow(): Promise<void>;
|
|
42
|
+
watch(kind: SlopStoreKind, callback: (event: SlopChange) => void): () => void;
|
|
43
|
+
}
|
|
44
|
+
export type WindowSlop = {
|
|
45
|
+
db: {
|
|
46
|
+
query: (sql: string, parameters?: unknown[]) => Promise<unknown[]>;
|
|
47
|
+
execute: (sql: string, parameters?: unknown[]) => Promise<number>;
|
|
48
|
+
transaction: (statements: SlopStatement[]) => Promise<number>;
|
|
49
|
+
onChange: (callback: (event: SlopChange) => void) => () => void;
|
|
50
|
+
};
|
|
51
|
+
json: {
|
|
52
|
+
open: (initialValue: unknown) => Promise<SlopSnapshot<unknown>>;
|
|
53
|
+
read: () => Promise<SlopSnapshot<unknown>>;
|
|
54
|
+
write: (value: unknown, expectedRevision?: string) => Promise<{
|
|
55
|
+
revision: string;
|
|
56
|
+
}>;
|
|
57
|
+
onChange: (callback: (event: SlopChange) => void) => () => void;
|
|
58
|
+
};
|
|
59
|
+
media: {
|
|
60
|
+
open: (name: string) => Promise<SlopMediaSnapshot>;
|
|
61
|
+
write: (name: string, data: string, mimeType: string) => Promise<{
|
|
62
|
+
revision: string;
|
|
63
|
+
}>;
|
|
64
|
+
remove: (name: string) => Promise<{
|
|
65
|
+
revision: null;
|
|
66
|
+
}>;
|
|
67
|
+
onChange: (callback: (event: SlopChange) => void) => () => void;
|
|
68
|
+
};
|
|
69
|
+
window?: {
|
|
70
|
+
resize: (size: SlopWindowSize) => Promise<SlopWindowSize>;
|
|
71
|
+
drag?: () => Promise<void>;
|
|
72
|
+
};
|
|
73
|
+
ready?: () => void;
|
|
74
|
+
};
|
|
75
|
+
declare global {
|
|
76
|
+
interface Window {
|
|
77
|
+
slop?: WindowSlop;
|
|
78
|
+
}
|
|
79
|
+
}
|
package/dist/types.js
ADDED
|
File without changes
|
package/package.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@hitslop/runtime",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Framework-neutral browser bridge for hitSlop documents.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/hitslop/hitslop.git",
|
|
9
|
+
"directory": "packages/runtime"
|
|
10
|
+
},
|
|
11
|
+
"homepage": "https://hitslop.app",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/hitslop/hitslop/issues"
|
|
14
|
+
},
|
|
15
|
+
"keywords": ["hitslop", "local-first", "webview", "mini-apps"],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
18
|
+
},
|
|
19
|
+
"type": "module",
|
|
20
|
+
"files": ["dist"],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": "./dist/index.js",
|
|
23
|
+
"./adapter": "./dist/adapter.js"
|
|
24
|
+
},
|
|
25
|
+
"scripts": {
|
|
26
|
+
"build": "bun ../../scripts/clean-dist.ts && tsc -p tsconfig.build.json",
|
|
27
|
+
"check": "tsc -p tsconfig.json",
|
|
28
|
+
"test": "bun test"
|
|
29
|
+
},
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"typescript": "^7.0.2"
|
|
32
|
+
}
|
|
33
|
+
}
|