@kokimoki/app 0.0.0 → 0.0.1

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.
@@ -0,0 +1,2 @@
1
+ import type { FetchParams } from "../types";
2
+ export declare function abortMediaUpload(id: string, { apiUrl, appToken, signal }: FetchParams): Promise<void>;
@@ -0,0 +1,12 @@
1
+ import { getHeaders } from "../utils";
2
+ export async function abortMediaUpload(id, { apiUrl, appToken, signal }) {
3
+ const res = await fetch(`${apiUrl}/upload/${id}`, {
4
+ method: "DELETE",
5
+ headers: getHeaders(appToken),
6
+ signal,
7
+ });
8
+ // TODO: Improve error handling
9
+ if (res.status !== 200) {
10
+ throw await res.json();
11
+ }
12
+ }
@@ -0,0 +1,2 @@
1
+ import type { AuthFetchParams, AuthResponse } from "../types";
2
+ export declare function fetchAuth({ apiUrl, clientToken, appId, }: AuthFetchParams): Promise<AuthResponse>;
@@ -0,0 +1,12 @@
1
+ import { getHeaders } from "../utils";
2
+ export async function fetchAuth({ apiUrl, clientToken, appId, }) {
3
+ const res = await fetch(`${apiUrl}/auth/token?appId=${appId}`, {
4
+ method: "GET",
5
+ headers: getHeaders(clientToken),
6
+ });
7
+ // TODO: Improve error handling
8
+ if (res.status !== 200) {
9
+ throw await res.json();
10
+ }
11
+ return await res.json();
12
+ }
@@ -0,0 +1,2 @@
1
+ import type { SendUploadResponse } from "../types";
2
+ export declare function getETags(blob: Blob, { urls, chunkSize }: SendUploadResponse): Promise<string[]>;
@@ -0,0 +1,18 @@
1
+ // TODO: Improve method below
2
+ export async function getETags(blob, { urls, chunkSize }) {
3
+ const uploadData = [];
4
+ for (let index = 0; index < urls.length; ++index) {
5
+ const url = urls[index];
6
+ uploadData.push({
7
+ url,
8
+ body: index < urls.length - 1
9
+ ? blob.slice(index * chunkSize, (index + 1) * chunkSize)
10
+ : blob.slice(index * chunkSize),
11
+ });
12
+ }
13
+ const responses = await Promise.all(uploadData.map(({ url, body }) => fetch(url, {
14
+ method: "PUT",
15
+ body,
16
+ })));
17
+ return responses.map((response) => JSON.parse(response.headers.get("ETag") || '""'));
18
+ }
@@ -0,0 +1,2 @@
1
+ import type { FetchParams, SendUploadResponse } from "../types";
2
+ export declare function sendMediaUpload(blob: Blob, { apiUrl, appToken, signal }: FetchParams): Promise<SendUploadResponse>;
@@ -0,0 +1,20 @@
1
+ import ObjectId from "bson-objectid";
2
+ import { getHeaders } from "../utils";
3
+ export async function sendMediaUpload(blob, { apiUrl, appToken, signal }) {
4
+ const objectId = new ObjectId().toHexString();
5
+ const res = await fetch(`${apiUrl}/upload`, {
6
+ method: "POST",
7
+ headers: getHeaders(appToken),
8
+ body: JSON.stringify({
9
+ name: objectId,
10
+ size: blob.size,
11
+ mimeType: blob.type,
12
+ }),
13
+ signal,
14
+ });
15
+ // TODO: Improve error handling
16
+ if (res.status !== 200) {
17
+ throw await res.json();
18
+ }
19
+ return await res.json();
20
+ }
@@ -0,0 +1,2 @@
1
+ import type { FetchParams, UploadMediaRequest, UploadMediaResponse } from "../types";
2
+ export declare function uploadMedia({ id, etags }: UploadMediaRequest, { apiUrl, appToken, signal }: FetchParams): Promise<UploadMediaResponse>;
@@ -0,0 +1,14 @@
1
+ import { getHeaders } from "../utils";
2
+ export async function uploadMedia({ id, etags }, { apiUrl, appToken, signal }) {
3
+ const res = await fetch(`${apiUrl}/upload/${id}`, {
4
+ method: "PUT",
5
+ headers: getHeaders(appToken),
6
+ body: JSON.stringify({ etags }),
7
+ signal,
8
+ });
9
+ // TODO: Improve error handling
10
+ if (res.status !== 200) {
11
+ throw await res.json();
12
+ }
13
+ return await res.json();
14
+ }
@@ -0,0 +1,67 @@
1
+ import type { Task } from "./types/tasks";
2
+ export interface FieldOptions {
3
+ label?: string;
4
+ optional: boolean;
5
+ }
6
+ export declare abstract class Field<T> {
7
+ readonly options: FieldOptions;
8
+ constructor(options: FieldOptions);
9
+ abstract get value(): T;
10
+ abstract get schema(): any;
11
+ }
12
+ export declare class ImageField extends Field<string> {
13
+ value: string;
14
+ constructor(value: string, options?: FieldOptions);
15
+ get schema(): {
16
+ type: string;
17
+ default: string;
18
+ };
19
+ }
20
+ export declare class TextField extends Field<string> {
21
+ value: string;
22
+ constructor(value: string, options?: FieldOptions);
23
+ get schema(): {
24
+ type: string;
25
+ default: string;
26
+ };
27
+ }
28
+ export declare class IntegerField extends Field<number> {
29
+ value: number;
30
+ constructor(value: number, options?: FieldOptions);
31
+ get schema(): {
32
+ type: string;
33
+ default: number;
34
+ };
35
+ }
36
+ export declare class FormGroup<T extends Record<string, any>> extends Field<{
37
+ [key in keyof T]: Field<T>["value"];
38
+ }> {
39
+ fields: T;
40
+ constructor(fields: T, options?: FieldOptions);
41
+ get value(): {
42
+ [key in keyof T]: T[key]["value"];
43
+ };
44
+ get schema(): {
45
+ type: string;
46
+ properties: any;
47
+ };
48
+ }
49
+ export declare class FormArray<T> extends Field<T[]> {
50
+ private factory;
51
+ value: Field<T>["value"][];
52
+ constructor(factory: () => Field<T>, value: Field<T>["value"][], options?: FieldOptions);
53
+ get schema(): {
54
+ type: string;
55
+ items: any;
56
+ };
57
+ }
58
+ export declare class TaskField extends Field<Task> {
59
+ value: Task;
60
+ constructor(value: Task, options?: FieldOptions);
61
+ get schema(): {
62
+ type: string;
63
+ properties: {};
64
+ };
65
+ }
66
+ export declare class Form<T extends Record<string, Field<any>>> extends FormGroup<T> {
67
+ }
package/dist/fields.js ADDED
@@ -0,0 +1,102 @@
1
+ const defaultFieldOptions = {
2
+ optional: false,
3
+ };
4
+ export class Field {
5
+ options;
6
+ constructor(options) {
7
+ this.options = options;
8
+ }
9
+ }
10
+ export class ImageField extends Field {
11
+ value;
12
+ constructor(value, options = defaultFieldOptions) {
13
+ super(options);
14
+ this.value = value;
15
+ }
16
+ get schema() {
17
+ return {
18
+ type: "string",
19
+ default: this.value,
20
+ };
21
+ }
22
+ }
23
+ export class TextField extends Field {
24
+ value;
25
+ constructor(value, options = defaultFieldOptions) {
26
+ super(options);
27
+ this.value = value;
28
+ }
29
+ get schema() {
30
+ return {
31
+ type: "string",
32
+ default: this.value,
33
+ };
34
+ }
35
+ }
36
+ export class IntegerField extends Field {
37
+ value;
38
+ constructor(value, options = defaultFieldOptions) {
39
+ super(options);
40
+ this.value = value;
41
+ }
42
+ get schema() {
43
+ return {
44
+ type: "integer",
45
+ default: this.value,
46
+ };
47
+ }
48
+ }
49
+ export class FormGroup extends Field {
50
+ fields;
51
+ constructor(fields, options = defaultFieldOptions) {
52
+ super(options);
53
+ this.fields = fields;
54
+ }
55
+ get value() {
56
+ return Object.entries(this.fields).reduce((acc, [key, field]) => {
57
+ acc[key] = field.value;
58
+ return acc;
59
+ }, {});
60
+ }
61
+ get schema() {
62
+ return {
63
+ type: "object",
64
+ properties: Object.entries(this.fields).reduce((acc, [key, field]) => {
65
+ acc[key] = field.schema;
66
+ return acc;
67
+ }, {}),
68
+ };
69
+ }
70
+ }
71
+ export class FormArray extends Field {
72
+ factory;
73
+ value;
74
+ constructor(factory, value, options = defaultFieldOptions) {
75
+ super(options);
76
+ this.factory = factory;
77
+ this.value = value;
78
+ }
79
+ get schema() {
80
+ const field = this.factory();
81
+ return {
82
+ type: "array",
83
+ items: field.schema,
84
+ };
85
+ }
86
+ }
87
+ export class TaskField extends Field {
88
+ value;
89
+ constructor(value, options = defaultFieldOptions) {
90
+ super(options);
91
+ this.value = value;
92
+ }
93
+ get schema() {
94
+ // TODO: validate task interface, not important right now
95
+ return {
96
+ type: "object",
97
+ properties: {},
98
+ };
99
+ }
100
+ }
101
+ export class Form extends FormGroup {
102
+ }
@@ -1,4 +1,4 @@
1
- export * from "./tasks";
1
+ export * from "./types";
2
2
  export * from "./fields";
3
3
  export * from "./kokimoki-client";
4
4
  export * from "./synced-store";
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export * from "./types";
2
+ export * from "./fields";
3
+ export * from "./kokimoki-client";
4
+ export * from "./synced-store";
5
+ export * from "@syncedstore/core";
@@ -0,0 +1,30 @@
1
+ import { HocuspocusProvider } from "@hocuspocus/provider";
2
+ import type TypedEmitter from "typed-emitter";
3
+ import { SyncedStore } from "./synced-store";
4
+ import type { DocTypeDescription } from "@syncedstore/core/types/doc";
5
+ import { UploadedMedia } from "./types";
6
+ export type KokimokiClientEvents<T> = {
7
+ stateless: (room: string, from: string, data: T) => void;
8
+ };
9
+ declare const KokimokiClient_base: new <T>() => TypedEmitter<KokimokiClientEvents<T>>;
10
+ export declare class KokimokiClient<StatelessDataT = any> extends KokimokiClient_base<StatelessDataT> {
11
+ readonly appId: string;
12
+ serverTimeOffset: number;
13
+ wsUrl: string;
14
+ apiUrl: string;
15
+ private _id;
16
+ private _token;
17
+ private _providers;
18
+ constructor(host: string, appId: string);
19
+ get id(): string;
20
+ get token(): string;
21
+ connect(): Promise<void>;
22
+ setProvider<T extends DocTypeDescription>(name: string, store: SyncedStore<T>): Promise<void>;
23
+ removeProvider(name: string): void;
24
+ getProvider(name: string): HocuspocusProvider;
25
+ serverTimestamp(): number;
26
+ sendStatelessToClient(room: string, clientId: string, data: StatelessDataT): void;
27
+ sendStatelessToRoom(room: string, data: StatelessDataT, self?: boolean): void;
28
+ mediaUpload(blob: Blob): Promise<UploadedMedia>;
29
+ }
30
+ export {};
@@ -0,0 +1,116 @@
1
+ import { HocuspocusProvider } from "@hocuspocus/provider";
2
+ import EventEmitter from "events";
3
+ import { sendMediaUpload } from "./api/sendMediaUpload";
4
+ import { getETags } from "./api/getETags";
5
+ import { uploadMedia } from "./api/uploadMedia";
6
+ import { errorMessage } from "./utils";
7
+ import { fetchAuth } from "./api/fetchAuth";
8
+ export class KokimokiClient extends EventEmitter {
9
+ appId;
10
+ serverTimeOffset = 0;
11
+ wsUrl;
12
+ apiUrl;
13
+ _id = "";
14
+ _token = "";
15
+ _providers = new Map();
16
+ constructor(host, appId) {
17
+ super();
18
+ this.appId = appId;
19
+ const secure = !host.match(/^localhost/);
20
+ this.wsUrl = `ws${secure ? "s" : ""}://${host}`;
21
+ this.apiUrl = `http${secure ? "s" : ""}://${host}`;
22
+ }
23
+ get id() {
24
+ if (!this._id) {
25
+ throw new Error("Client not connected");
26
+ }
27
+ return this._id;
28
+ }
29
+ get token() {
30
+ if (!this._token) {
31
+ throw new Error("Client not connected");
32
+ }
33
+ return this._token;
34
+ }
35
+ async connect() {
36
+ let clientToken = localStorage.getItem("KM_TOKEN");
37
+ const startTime = Date.now();
38
+ const { clientId, appToken, serverTime, token } = await fetchAuth({
39
+ appId: this.appId,
40
+ clientToken,
41
+ apiUrl: this.apiUrl,
42
+ });
43
+ const endTime = Date.now();
44
+ const ping = Math.round((endTime - startTime) / 2);
45
+ this._id = clientId;
46
+ this._token = appToken;
47
+ this.serverTimeOffset = Date.now() - serverTime - ping;
48
+ localStorage.setItem("KM_TOKEN", token);
49
+ console.log("READY", token);
50
+ }
51
+ async setProvider(name, store) {
52
+ const provider = new HocuspocusProvider({
53
+ url: `${this.wsUrl}/connection`,
54
+ name: `${this.appId}/${name}`,
55
+ document: store.doc,
56
+ token: this.token,
57
+ });
58
+ this._providers.set(name, provider);
59
+ // Handle incoming stateless messages
60
+ provider.on("stateless", (e) => {
61
+ const payload = JSON.parse(e.payload);
62
+ this.emit("stateless", name, payload.from, payload.data);
63
+ });
64
+ // Wait for initial sync
65
+ await new Promise((resolve) => {
66
+ const handler = () => {
67
+ provider.off("synced", handler);
68
+ resolve();
69
+ };
70
+ provider.on("synced", handler);
71
+ });
72
+ }
73
+ removeProvider(name) {
74
+ const provider = this._providers.get(name);
75
+ if (!provider) {
76
+ throw new Error(`No provider for room ${name}`);
77
+ }
78
+ provider.destroy();
79
+ this._providers.delete(name);
80
+ }
81
+ getProvider(name) {
82
+ return this._providers.get(name);
83
+ }
84
+ serverTimestamp() {
85
+ return Date.now() - this.serverTimeOffset;
86
+ }
87
+ sendStatelessToClient(room, clientId, data) {
88
+ const provider = this._providers.get(room);
89
+ if (!provider) {
90
+ throw new Error(`No provider for room ${room}`);
91
+ }
92
+ provider.sendStateless(JSON.stringify({ to: clientId, data }));
93
+ }
94
+ sendStatelessToRoom(room, data, self = false) {
95
+ const provider = this._providers.get(room);
96
+ if (!provider) {
97
+ throw new Error(`No provider for room ${room}`);
98
+ }
99
+ provider.sendStateless(JSON.stringify({ data, self }));
100
+ }
101
+ async mediaUpload(blob) {
102
+ try {
103
+ const uploadData = await sendMediaUpload(blob, {
104
+ apiUrl: this.apiUrl,
105
+ appToken: this.token,
106
+ });
107
+ const { id } = uploadData;
108
+ const etags = await getETags(blob, uploadData);
109
+ const { url } = await uploadMedia({ id, etags }, { apiUrl: this.apiUrl, appToken: this.token });
110
+ return { url, id };
111
+ }
112
+ catch (error) {
113
+ throw new Error(`Error while uploading media: ${errorMessage(error)}`);
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,29 @@
1
+ import { HocuspocusProvider } from "@hocuspocus/provider";
2
+ import type { Y } from "@syncedstore/core";
3
+ import type TypedEmitter from "typed-emitter";
4
+ import type { UploadedMedia } from "./types";
5
+ type KokimokiClientEvents<T> = {
6
+ stateless: (room: string, from: string, data: T) => void;
7
+ };
8
+ declare const KokimokiClient_base: new <T>() => TypedEmitter<KokimokiClientEvents<T>>;
9
+ export declare class KokimokiClient<StatelessDataT = any> extends KokimokiClient_base<StatelessDataT> {
10
+ readonly appId: string;
11
+ serverTimeOffset: number;
12
+ wsUrl: string;
13
+ apiUrl: string;
14
+ private _id;
15
+ private _token;
16
+ private _providers;
17
+ constructor(host: string, appId: string);
18
+ get id(): string;
19
+ get token(): string;
20
+ connect(): Promise<void>;
21
+ setProvider(name: string, document: Y.Doc): Promise<void>;
22
+ removeProvider(name: string): void;
23
+ getProvider(name: string): HocuspocusProvider;
24
+ serverTimestamp(): number;
25
+ sendStatelessToClient(room: string, clientId: string, data: StatelessDataT): void;
26
+ sendStatelessToRoom(room: string, data: StatelessDataT, self?: boolean): void;
27
+ mediaUpload(blob: Blob): Promise<UploadedMedia>;
28
+ }
29
+ export {};
@@ -0,0 +1,116 @@
1
+ import { HocuspocusProvider } from "@hocuspocus/provider";
2
+ import EventEmitter from "events";
3
+ import { errorMessage } from "./utils";
4
+ import { fetchAuth } from "./api/fetchAuth";
5
+ import { sendMediaUpload } from "./api/sendMediaUpload";
6
+ import { getETags } from "./api/getETags";
7
+ import { uploadMedia } from "./api/uploadMedia";
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ export class KokimokiClient extends EventEmitter {
10
+ appId;
11
+ serverTimeOffset = 0;
12
+ wsUrl;
13
+ apiUrl;
14
+ _id = "";
15
+ _token = "";
16
+ _providers = new Map();
17
+ constructor(host, appId) {
18
+ super();
19
+ this.appId = appId;
20
+ const secure = !host.match(/^localhost/);
21
+ this.wsUrl = `ws${secure ? "s" : ""}://${host}`;
22
+ this.apiUrl = `http${secure ? "s" : ""}://${host}`;
23
+ }
24
+ get id() {
25
+ if (!this._id) {
26
+ throw new Error("Client not connected");
27
+ }
28
+ return this._id;
29
+ }
30
+ get token() {
31
+ if (!this._token) {
32
+ throw new Error("Client not connected");
33
+ }
34
+ return this._token;
35
+ }
36
+ async connect() {
37
+ const clientToken = localStorage.getItem("KM_TOKEN");
38
+ const startTime = Date.now();
39
+ const { clientId, appToken, serverTime, token } = await fetchAuth({
40
+ appId: this.appId,
41
+ clientToken,
42
+ apiUrl: this.apiUrl,
43
+ });
44
+ const endTime = Date.now();
45
+ const ping = Math.round((endTime - startTime) / 2);
46
+ this._id = clientId;
47
+ this._token = appToken;
48
+ this.serverTimeOffset = Date.now() - serverTime - ping;
49
+ localStorage.setItem("KM_TOKEN", token);
50
+ }
51
+ async setProvider(name, document) {
52
+ const provider = new HocuspocusProvider({
53
+ url: `${this.wsUrl}/connection`,
54
+ name: `${this.appId}/${name}`,
55
+ document,
56
+ token: this.token,
57
+ });
58
+ this._providers.set(name, provider);
59
+ // Handle incoming stateless messages
60
+ provider.on("stateless", (e) => {
61
+ const payload = JSON.parse(e.payload);
62
+ this.emit("stateless", name, payload.from, payload.data);
63
+ });
64
+ // Wait for initial sync
65
+ await new Promise((resolve) => {
66
+ const handler = () => {
67
+ provider.off("synced", handler);
68
+ resolve();
69
+ };
70
+ provider.on("synced", handler);
71
+ });
72
+ }
73
+ removeProvider(name) {
74
+ const provider = this._providers.get(name);
75
+ if (!provider) {
76
+ throw new Error(`No provider for room ${name}`);
77
+ }
78
+ provider.destroy();
79
+ this._providers.delete(name);
80
+ }
81
+ getProvider(name) {
82
+ return this._providers.get(name);
83
+ }
84
+ serverTimestamp() {
85
+ return Date.now() - this.serverTimeOffset;
86
+ }
87
+ sendStatelessToClient(room, clientId, data) {
88
+ const provider = this._providers.get(room);
89
+ if (!provider) {
90
+ throw new Error(`No provider for room ${room}`);
91
+ }
92
+ provider.sendStateless(JSON.stringify({ to: clientId, data }));
93
+ }
94
+ sendStatelessToRoom(room, data, self = false) {
95
+ const provider = this._providers.get(room);
96
+ if (!provider) {
97
+ throw new Error(`No provider for room ${room}`);
98
+ }
99
+ provider.sendStateless(JSON.stringify({ data, self }));
100
+ }
101
+ async mediaUpload(blob) {
102
+ try {
103
+ const uploadData = await sendMediaUpload(blob, {
104
+ apiUrl: this.apiUrl,
105
+ appToken: this.token,
106
+ });
107
+ const { id } = uploadData;
108
+ const etags = await getETags(blob, uploadData);
109
+ const { url } = await uploadMedia({ id, etags }, { apiUrl: this.apiUrl, appToken: this.token });
110
+ return { url, id };
111
+ }
112
+ catch (error) {
113
+ throw new Error(`Error while uploading media: ${errorMessage(error)}`);
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,30 @@
1
+ import { HocuspocusProvider } from "@hocuspocus/provider";
2
+ import type TypedEmitter from "typed-emitter";
3
+ import { SyncedStore } from "./synced-store";
4
+ import type { DocTypeDescription } from "@syncedstore/core/types/doc";
5
+ import { UploadedMedia } from "./types";
6
+ export type KokimokiClientEvents<T> = {
7
+ stateless: (room: string, from: string, data: T) => void;
8
+ };
9
+ declare const KokimokiClient_base: new <T>() => TypedEmitter<KokimokiClientEvents<T>>;
10
+ export declare class KokimokiClient<StatelessDataT = any> extends KokimokiClient_base<StatelessDataT> {
11
+ readonly appId: string;
12
+ serverTimeOffset: number;
13
+ wsUrl: string;
14
+ apiUrl: string;
15
+ private _id;
16
+ private _token;
17
+ private _providers;
18
+ constructor(host: string, appId: string);
19
+ get id(): string;
20
+ get token(): string;
21
+ connect(): Promise<void>;
22
+ setProvider<T extends DocTypeDescription>(name: string, store: SyncedStore<T>): Promise<void>;
23
+ removeProvider(name: string): void;
24
+ getProvider(name: string): HocuspocusProvider;
25
+ serverTimestamp(): number;
26
+ sendStatelessToClient(room: string, clientId: string, data: StatelessDataT): void;
27
+ sendStatelessToRoom(room: string, data: StatelessDataT, self?: boolean): void;
28
+ mediaUpload(blob: Blob): Promise<UploadedMedia>;
29
+ }
30
+ export {};
@@ -0,0 +1,116 @@
1
+ import { HocuspocusProvider } from "@hocuspocus/provider";
2
+ import EventEmitter from "events";
3
+ import { sendMediaUpload } from "./api/sendMediaUpload";
4
+ import { getETags } from "./api/getETags";
5
+ import { uploadMedia } from "./api/uploadMedia";
6
+ import { errorMessage } from "./utils";
7
+ import { fetchAuth } from "./api/fetchAuth";
8
+ export class KokimokiClient extends EventEmitter {
9
+ appId;
10
+ serverTimeOffset = 0;
11
+ wsUrl;
12
+ apiUrl;
13
+ _id = "";
14
+ _token = "";
15
+ _providers = new Map();
16
+ constructor(host, appId) {
17
+ super();
18
+ this.appId = appId;
19
+ const secure = !host.match(/^localhost/);
20
+ this.wsUrl = `ws${secure ? "s" : ""}://${host}`;
21
+ this.apiUrl = `http${secure ? "s" : ""}://${host}`;
22
+ }
23
+ get id() {
24
+ if (!this._id) {
25
+ throw new Error("Client not connected");
26
+ }
27
+ return this._id;
28
+ }
29
+ get token() {
30
+ if (!this._token) {
31
+ throw new Error("Client not connected");
32
+ }
33
+ return this._token;
34
+ }
35
+ async connect() {
36
+ let clientToken = localStorage.getItem("KM_TOKEN");
37
+ const startTime = Date.now();
38
+ const { clientId, appToken, serverTime, token } = await fetchAuth({
39
+ appId: this.appId,
40
+ clientToken,
41
+ apiUrl: this.apiUrl,
42
+ });
43
+ const endTime = Date.now();
44
+ const ping = Math.round((endTime - startTime) / 2);
45
+ this._id = clientId;
46
+ this._token = appToken;
47
+ this.serverTimeOffset = Date.now() - serverTime - ping;
48
+ localStorage.setItem("KM_TOKEN", token);
49
+ console.log("READY!", token);
50
+ }
51
+ async setProvider(name, store) {
52
+ const provider = new HocuspocusProvider({
53
+ url: `${this.wsUrl}/connection`,
54
+ name: `${this.appId}/${name}`,
55
+ document: store.doc,
56
+ token: this.token,
57
+ });
58
+ this._providers.set(name, provider);
59
+ // Handle incoming stateless messages
60
+ provider.on("stateless", (e) => {
61
+ const payload = JSON.parse(e.payload);
62
+ this.emit("stateless", name, payload.from, payload.data);
63
+ });
64
+ // Wait for initial sync
65
+ await new Promise((resolve) => {
66
+ const handler = () => {
67
+ provider.off("synced", handler);
68
+ resolve();
69
+ };
70
+ provider.on("synced", handler);
71
+ });
72
+ }
73
+ removeProvider(name) {
74
+ const provider = this._providers.get(name);
75
+ if (!provider) {
76
+ throw new Error(`No provider for room ${name}`);
77
+ }
78
+ provider.destroy();
79
+ this._providers.delete(name);
80
+ }
81
+ getProvider(name) {
82
+ return this._providers.get(name);
83
+ }
84
+ serverTimestamp() {
85
+ return Date.now() - this.serverTimeOffset;
86
+ }
87
+ sendStatelessToClient(room, clientId, data) {
88
+ const provider = this._providers.get(room);
89
+ if (!provider) {
90
+ throw new Error(`No provider for room ${room}`);
91
+ }
92
+ provider.sendStateless(JSON.stringify({ to: clientId, data }));
93
+ }
94
+ sendStatelessToRoom(room, data, self = false) {
95
+ const provider = this._providers.get(room);
96
+ if (!provider) {
97
+ throw new Error(`No provider for room ${room}`);
98
+ }
99
+ provider.sendStateless(JSON.stringify({ data, self }));
100
+ }
101
+ async mediaUpload(blob) {
102
+ try {
103
+ const uploadData = await sendMediaUpload(blob, {
104
+ apiUrl: this.apiUrl,
105
+ appToken: this.token,
106
+ });
107
+ const { id } = uploadData;
108
+ const etags = await getETags(blob, uploadData);
109
+ const { url } = await uploadMedia({ id, etags }, { apiUrl: this.apiUrl, appToken: this.token });
110
+ return { url, id };
111
+ }
112
+ catch (error) {
113
+ throw new Error(`Error while uploading media: ${errorMessage(error)}`);
114
+ }
115
+ }
116
+ }
@@ -0,0 +1,7 @@
1
+ import * as Y from "yjs";
2
+ import type { DocTypeDescription, MappedTypeDescription } from "@syncedstore/core/types/doc";
3
+ export declare class SyncedStore<T extends DocTypeDescription> {
4
+ readonly data: MappedTypeDescription<T>;
5
+ readonly doc: Y.Doc;
6
+ constructor(initialState: T);
7
+ }
@@ -0,0 +1,9 @@
1
+ import { syncedStore, getYjsDoc } from "@syncedstore/core";
2
+ export class SyncedStore {
3
+ data;
4
+ doc;
5
+ constructor(initialState) {
6
+ this.data = syncedStore(initialState);
7
+ this.doc = getYjsDoc(this.data);
8
+ }
9
+ }
@@ -0,0 +1,43 @@
1
+ export interface Answer<T = any> {
2
+ value?: T;
3
+ correct: boolean;
4
+ }
5
+ export interface TaskBase {
6
+ id: string;
7
+ text: string;
8
+ points: number;
9
+ metadata?: Record<string, any>;
10
+ }
11
+ export interface TaskOption {
12
+ text: string;
13
+ correct: boolean;
14
+ }
15
+ export interface MultipleChoiceTask extends TaskBase {
16
+ type: "multiple-choice";
17
+ answers: TaskOption[];
18
+ }
19
+ export interface SingleChoiceTask extends TaskBase {
20
+ type: "single-choice";
21
+ answers: TaskOption[];
22
+ }
23
+ export interface TextTask extends TaskBase {
24
+ type: "text";
25
+ answer: string;
26
+ }
27
+ export interface TextSurveyTask extends TaskBase {
28
+ type: "text-survey";
29
+ }
30
+ export interface NumericTask extends TaskBase {
31
+ type: "numeric";
32
+ answer: number;
33
+ }
34
+ export interface PhotoTask extends TaskBase {
35
+ type: "photo";
36
+ }
37
+ export interface VideoTask extends TaskBase {
38
+ type: "video";
39
+ }
40
+ export interface InfoTask extends TaskBase {
41
+ type: "info";
42
+ }
43
+ export type Task = MultipleChoiceTask | SingleChoiceTask | TextTask | NumericTask | TextSurveyTask | PhotoTask | VideoTask | InfoTask;
package/dist/tasks.js ADDED
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,36 @@
1
+ export interface SendUploadResponse {
2
+ id: string;
3
+ urls: string[];
4
+ chunkSize: number;
5
+ }
6
+ export interface UploadMediaResponse {
7
+ url: string;
8
+ }
9
+ export interface AuthResponse {
10
+ serverTime: number;
11
+ appToken: string;
12
+ token: string;
13
+ clientId: string;
14
+ }
15
+ export interface MediaUploadData {
16
+ url: string;
17
+ body: Blob;
18
+ }
19
+ export interface FetchParams {
20
+ apiUrl: string;
21
+ appToken: string;
22
+ signal?: AbortSignal;
23
+ }
24
+ export interface AuthFetchParams {
25
+ apiUrl: string;
26
+ clientToken: string | null;
27
+ appId: string;
28
+ }
29
+ export interface UploadMediaRequest {
30
+ etags: string[];
31
+ id: string;
32
+ }
33
+ export interface UploadedMedia {
34
+ id: string;
35
+ url: string;
36
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,2 @@
1
+ export * from "./api";
2
+ export * from "./tasks";
@@ -0,0 +1,2 @@
1
+ export * from "./api";
2
+ export * from "./tasks";
@@ -0,0 +1,43 @@
1
+ export interface Answer<T = any> {
2
+ value?: T;
3
+ correct: boolean;
4
+ }
5
+ export interface TaskBase {
6
+ id: string;
7
+ text: string;
8
+ points: number;
9
+ metadata?: Record<string, any>;
10
+ }
11
+ export interface TaskOption {
12
+ text: string;
13
+ correct: boolean;
14
+ }
15
+ export interface MultipleChoiceTask extends TaskBase {
16
+ type: "multiple-choice";
17
+ answers: TaskOption[];
18
+ }
19
+ export interface SingleChoiceTask extends TaskBase {
20
+ type: "single-choice";
21
+ answers: TaskOption[];
22
+ }
23
+ export interface TextTask extends TaskBase {
24
+ type: "text";
25
+ answer: string;
26
+ }
27
+ export interface TextSurveyTask extends TaskBase {
28
+ type: "text-survey";
29
+ }
30
+ export interface NumericTask extends TaskBase {
31
+ type: "numeric";
32
+ answer: number;
33
+ }
34
+ export interface PhotoTask extends TaskBase {
35
+ type: "photo";
36
+ }
37
+ export interface VideoTask extends TaskBase {
38
+ type: "video";
39
+ }
40
+ export interface InfoTask extends TaskBase {
41
+ type: "info";
42
+ }
43
+ export type Task = MultipleChoiceTask | SingleChoiceTask | TextTask | NumericTask | TextSurveyTask | PhotoTask | VideoTask | InfoTask;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,6 @@
1
+ export declare function wait(msDelay: number): Promise<unknown>;
2
+ export declare function hasOwnProperty<P extends PropertyKey>(obj: object, property: P): obj is {
3
+ [p in P]: unknown;
4
+ };
5
+ export declare function getHeaders(token: string | null): Headers;
6
+ export declare function errorMessage(error: unknown): string;
package/dist/utils.js ADDED
@@ -0,0 +1,25 @@
1
+ export function wait(msDelay) {
2
+ return new Promise((resolve) => setTimeout(resolve, msDelay));
3
+ }
4
+ export function hasOwnProperty(obj, property) {
5
+ return Object.prototype.hasOwnProperty.call(obj, property);
6
+ }
7
+ export function getHeaders(token) {
8
+ return token
9
+ ? new Headers({
10
+ Authorization: `Bearer ${token}`,
11
+ "Content-Type": "application/json",
12
+ })
13
+ : new Headers();
14
+ }
15
+ export function errorMessage(error) {
16
+ if (typeof error === "string")
17
+ return error;
18
+ if (typeof error !== "object" && typeof error !== "function")
19
+ return "Unknown error";
20
+ // TODO: Extend errors check
21
+ if (error instanceof Error) {
22
+ return error.message;
23
+ }
24
+ return "Unknown error";
25
+ }
package/package.json CHANGED
@@ -1,8 +1,13 @@
1
1
  {
2
2
  "name": "@kokimoki/app",
3
- "version": "0.0.0",
3
+ "version": "0.0.1",
4
+ "type": "module",
4
5
  "description": "Kokimoki app",
5
- "main": "src/index.ts",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "files": [
9
+ "dist"
10
+ ],
6
11
  "scripts": {
7
12
  "test": "echo \"Error: no test specified\" && exit 1",
8
13
  "build": "tsc",
package/src/fields.ts DELETED
@@ -1,125 +0,0 @@
1
- import type { Task } from "./tasks";
2
-
3
- export interface FieldOptions {
4
- label?: string;
5
- optional: boolean;
6
- }
7
-
8
- const defaultFieldOptions: FieldOptions = {
9
- optional: false,
10
- };
11
-
12
- export abstract class Field<T> {
13
- constructor(public readonly options: FieldOptions) {}
14
-
15
- abstract get value(): T;
16
- abstract get schema(): any;
17
- }
18
-
19
- export class ImageField extends Field<string> {
20
- constructor(
21
- public value: string,
22
- options: FieldOptions = defaultFieldOptions
23
- ) {
24
- super(options);
25
- }
26
-
27
- get schema() {
28
- return {
29
- type: "string",
30
- default: this.value,
31
- };
32
- }
33
- }
34
-
35
- export class TextField extends Field<string> {
36
- constructor(
37
- public value: string,
38
- options: FieldOptions = defaultFieldOptions
39
- ) {
40
- super(options);
41
- }
42
-
43
- get schema() {
44
- return {
45
- type: "string",
46
- default: this.value,
47
- };
48
- }
49
- }
50
-
51
- export class IntegerField extends Field<number> {
52
- constructor(
53
- public value: number,
54
- options: FieldOptions = defaultFieldOptions
55
- ) {
56
- super(options);
57
- }
58
-
59
- get schema() {
60
- return {
61
- type: "integer",
62
- default: this.value,
63
- };
64
- }
65
- }
66
-
67
- export class FormGroup<T extends Record<string, any>> extends Field<{
68
- [key in keyof T]: Field<T>["value"];
69
- }> {
70
- constructor(public fields: T, options: FieldOptions = defaultFieldOptions) {
71
- super(options);
72
- }
73
-
74
- get value(): { [key in keyof T]: T[key]["value"] } {
75
- return Object.entries(this.fields).reduce((acc, [key, field]) => {
76
- acc[key] = field.value;
77
- return acc;
78
- }, {} as any);
79
- }
80
-
81
- get schema() {
82
- return {
83
- type: "object",
84
- properties: Object.entries(this.fields).reduce((acc, [key, field]) => {
85
- acc[key] = field.schema;
86
- return acc;
87
- }, {} as any),
88
- };
89
- }
90
- }
91
-
92
- export class FormArray<T> extends Field<T[]> {
93
- constructor(
94
- private factory: () => Field<T>,
95
- public value: Field<T>["value"][],
96
- options: FieldOptions = defaultFieldOptions
97
- ) {
98
- super(options);
99
- }
100
-
101
- get schema() {
102
- const field = this.factory();
103
-
104
- return {
105
- type: "array",
106
- items: field.schema,
107
- };
108
- }
109
- }
110
-
111
- export class TaskField extends Field<Task> {
112
- constructor(public value: Task, options: FieldOptions = defaultFieldOptions) {
113
- super(options);
114
- }
115
-
116
- get schema() {
117
- // TODO: validate task interface, not important right now
118
- return {
119
- type: "object",
120
- properties: {},
121
- };
122
- }
123
- }
124
-
125
- export class Form<T extends Record<string, Field<any>>> extends FormGroup<T> {}
@@ -1,134 +0,0 @@
1
- import { HocuspocusProvider } from "@hocuspocus/provider";
2
- import EventEmitter from "events";
3
- import type TypedEmitter from "typed-emitter";
4
- import { SyncedStore } from "./synced-store";
5
- import type { DocTypeDescription } from "@syncedstore/core/types/doc";
6
-
7
- type KokimokiClientEvents<T> = {
8
- stateless: (room: string, from: string, data: T) => void;
9
- };
10
-
11
- export class KokimokiClient<StatelessDataT = any> extends (EventEmitter as {
12
- new <T>(): TypedEmitter<KokimokiClientEvents<T>>;
13
- })<StatelessDataT> {
14
- public serverTimeOffset = 0;
15
- public wsUrl: string;
16
- public apiUrl: string;
17
-
18
- private _id = "";
19
- private _token = "";
20
- private _providers = new Map<string, HocuspocusProvider>();
21
-
22
- constructor(host: string, public readonly appId: string) {
23
- super();
24
- const secure = !host.match(/^localhost/);
25
- this.wsUrl = `ws${secure ? "s" : ""}://${host}`;
26
- this.apiUrl = `http${secure ? "s" : ""}://${host}`;
27
- }
28
-
29
- get id() {
30
- if (!this._id) {
31
- throw new Error("Client not connected");
32
- }
33
-
34
- return this._id;
35
- }
36
-
37
- get token() {
38
- if (!this._token) {
39
- throw new Error("Client not connected");
40
- }
41
-
42
- return this._token;
43
- }
44
-
45
- async connect() {
46
- let clientToken = localStorage.getItem("KM_TOKEN");
47
-
48
- const startTime = Date.now();
49
- const res = await fetch(`${this.apiUrl}/auth/token?appId=${this.appId}`, {
50
- headers: clientToken
51
- ? new Headers({ Authorization: `Bearer ${clientToken}` })
52
- : new Headers(),
53
- });
54
- const body = await res.json();
55
- const endTime = Date.now();
56
- const ping = Math.round((endTime - startTime) / 2);
57
-
58
- this._id = body.clientId;
59
- this._token = body.appToken;
60
- this.serverTimeOffset = Date.now() - body.serverTime - ping;
61
- localStorage.setItem("KM_TOKEN", body.token);
62
- }
63
-
64
- async setProvider<T extends DocTypeDescription>(
65
- name: string,
66
- store: SyncedStore<T>
67
- ) {
68
- const provider = new HocuspocusProvider({
69
- url: `${this.wsUrl}/connection`,
70
- name: `${this.appId}/${name}`,
71
- document: store.doc,
72
- token: this.token,
73
- });
74
-
75
- this._providers.set(name, provider);
76
-
77
- // Handle incoming stateless messages
78
- provider.on("stateless", (e: { payload: string }) => {
79
- const payload: { from: string; data: StatelessDataT } = JSON.parse(
80
- e.payload
81
- );
82
- this.emit("stateless", name, payload.from, payload.data);
83
- });
84
-
85
- // Wait for initial sync
86
- await new Promise<void>((resolve) => {
87
- const handler = () => {
88
- provider.off("synced", handler);
89
- resolve();
90
- };
91
-
92
- provider.on("synced", handler);
93
- });
94
- }
95
-
96
- removeProvider(name: string) {
97
- const provider = this._providers.get(name);
98
-
99
- if (!provider) {
100
- throw new Error(`No provider for room ${name}`);
101
- }
102
-
103
- provider.destroy();
104
- this._providers.delete(name);
105
- }
106
-
107
- getProvider(name: string) {
108
- return this._providers.get(name);
109
- }
110
-
111
- serverTimestamp() {
112
- return Date.now() - this.serverTimeOffset;
113
- }
114
-
115
- sendStatelessToClient(room: string, clientId: string, data: StatelessDataT) {
116
- const provider = this._providers.get(room);
117
-
118
- if (!provider) {
119
- throw new Error(`No provider for room ${room}`);
120
- }
121
-
122
- provider.sendStateless(JSON.stringify({ to: clientId, data }));
123
- }
124
-
125
- sendStatelessToRoom(room: string, data: StatelessDataT, self = false) {
126
- const provider = this._providers.get(room);
127
-
128
- if (!provider) {
129
- throw new Error(`No provider for room ${room}`);
130
- }
131
-
132
- provider.sendStateless(JSON.stringify({ data, self }));
133
- }
134
- }
@@ -1,16 +0,0 @@
1
- import * as Y from "yjs";
2
- import { syncedStore, getYjsDoc } from "@syncedstore/core";
3
- import type {
4
- DocTypeDescription,
5
- MappedTypeDescription,
6
- } from "@syncedstore/core/types/doc";
7
-
8
- export class SyncedStore<T extends DocTypeDescription> {
9
- public readonly data: MappedTypeDescription<T>;
10
- public readonly doc: Y.Doc;
11
-
12
- constructor(initialState: T) {
13
- this.data = syncedStore(initialState);
14
- this.doc = getYjsDoc(this.data);
15
- }
16
- }
package/src/tasks.ts DELETED
@@ -1,62 +0,0 @@
1
- export interface Answer<T = any> {
2
- value?: T;
3
- correct: boolean;
4
- }
5
-
6
- export interface TaskBase {
7
- id: string;
8
- text: string;
9
- points: number;
10
- metadata?: Record<string, any>;
11
- }
12
-
13
- export interface TaskOption {
14
- text: string;
15
- correct: boolean;
16
- }
17
-
18
- export interface MultipleChoiceTask extends TaskBase {
19
- type: "multiple-choice";
20
- answers: TaskOption[];
21
- }
22
-
23
- export interface SingleChoiceTask extends TaskBase {
24
- type: "single-choice";
25
- answers: TaskOption[];
26
- }
27
-
28
- export interface TextTask extends TaskBase {
29
- type: "text";
30
- answer: string;
31
- }
32
-
33
- export interface TextSurveyTask extends TaskBase {
34
- type: "text-survey";
35
- }
36
-
37
- export interface NumericTask extends TaskBase {
38
- type: "numeric";
39
- answer: number;
40
- }
41
-
42
- export interface PhotoTask extends TaskBase {
43
- type: "photo";
44
- }
45
-
46
- export interface VideoTask extends TaskBase {
47
- type: "video";
48
- }
49
-
50
- export interface InfoTask extends TaskBase {
51
- type: "info";
52
- }
53
-
54
- export type Task =
55
- | MultipleChoiceTask
56
- | SingleChoiceTask
57
- | TextTask
58
- | TextSurveyTask
59
- | NumericTask
60
- | PhotoTask
61
- | VideoTask
62
- | InfoTask;
package/tsconfig.json DELETED
@@ -1,8 +0,0 @@
1
- {
2
- "compilerOptions": {
3
- "outDir": "dist",
4
- "esModuleInterop": true,
5
- "declaration": true
6
- },
7
- "include": ["src/**/*.ts"]
8
- }