@dabble/patches 0.5.22 → 0.6.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/dist/algorithms/server/commitChanges.js +4 -5
- package/dist/algorithms/server/createVersion.js +2 -4
- package/dist/algorithms/server/handleOfflineSessionsAndBatches.js +5 -7
- package/dist/data/change.js +2 -3
- package/dist/index.d.ts +0 -1
- package/dist/index.js +0 -1
- package/dist/server/CompressedStoreBackend.d.ts +1 -1
- package/dist/server/PatchesBranchManager.js +1 -2
- package/dist/server/PatchesServer.js +1 -2
- package/dist/server/types.d.ts +1 -1
- package/dist/types.d.ts +14 -14
- package/package.json +1 -1
- package/dist/utils/dates.d.ts +0 -43
- package/dist/utils/dates.js +0 -47
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import "../../chunk-IZ2YBCUP.js";
|
|
2
|
-
import { clampTimestamp, getISO, timestampDiff } from "../../utils/dates.js";
|
|
3
2
|
import { filterSoftWritesAgainstState } from "../../json-patch/utils/softWrites.js";
|
|
4
3
|
import { applyChanges } from "../shared/applyChanges.js";
|
|
5
4
|
import { createVersion } from "./createVersion.js";
|
|
@@ -28,7 +27,7 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis, option
|
|
|
28
27
|
});
|
|
29
28
|
}
|
|
30
29
|
}
|
|
31
|
-
const serverNow =
|
|
30
|
+
const serverNow = Date.now();
|
|
32
31
|
let rev = baseRev + 1;
|
|
33
32
|
changes.forEach((c) => {
|
|
34
33
|
if (c.baseRev == null) c.baseRev = baseRev;
|
|
@@ -40,7 +39,7 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis, option
|
|
|
40
39
|
if (!options?.historicalImport || !c.committedAt) {
|
|
41
40
|
c.committedAt = serverNow;
|
|
42
41
|
}
|
|
43
|
-
c.createdAt = c.createdAt ?
|
|
42
|
+
c.createdAt = c.createdAt ? Math.min(c.createdAt, serverNow) : serverNow;
|
|
44
43
|
});
|
|
45
44
|
if (baseRev > currentRev) {
|
|
46
45
|
throw new Error(
|
|
@@ -54,7 +53,7 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis, option
|
|
|
54
53
|
}
|
|
55
54
|
const lastChange = currentChanges[currentChanges.length - 1];
|
|
56
55
|
const compareTime = options?.historicalImport ? changes[0].createdAt : serverNow;
|
|
57
|
-
if (lastChange &&
|
|
56
|
+
if (lastChange && compareTime - lastChange.createdAt > sessionTimeoutMillis) {
|
|
58
57
|
await createVersion(store, docId, currentState, currentChanges);
|
|
59
58
|
}
|
|
60
59
|
const committedChanges = await store.listChanges(docId, {
|
|
@@ -66,7 +65,7 @@ async function commitChanges(store, docId, changes, sessionTimeoutMillis, option
|
|
|
66
65
|
if (incomingChanges.length === 0) {
|
|
67
66
|
return [committedChanges, []];
|
|
68
67
|
}
|
|
69
|
-
const isOfflineTimestamp =
|
|
68
|
+
const isOfflineTimestamp = serverNow - incomingChanges[0].createdAt > sessionTimeoutMillis;
|
|
70
69
|
if (isOfflineTimestamp || batchId) {
|
|
71
70
|
const canFastForward = committedChanges.length === 0;
|
|
72
71
|
const origin = options?.historicalImport ? "main" : canFastForward ? "main" : "offline-branch";
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import "../../chunk-IZ2YBCUP.js";
|
|
2
2
|
import { createVersionMetadata } from "../../data/version.js";
|
|
3
|
-
import { getISO } from "../../utils/dates.js";
|
|
4
3
|
async function createVersion(store, docId, state, changes, metadata) {
|
|
5
4
|
if (changes.length === 0) return;
|
|
6
5
|
const startRev = changes[0].rev;
|
|
@@ -9,9 +8,8 @@ async function createVersion(store, docId, state, changes, metadata) {
|
|
|
9
8
|
}
|
|
10
9
|
const sessionMetadata = createVersionMetadata({
|
|
11
10
|
origin: "main",
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
endedAt: getISO(changes[changes.length - 1].createdAt),
|
|
11
|
+
startedAt: changes[0].createdAt,
|
|
12
|
+
endedAt: changes[changes.length - 1].createdAt,
|
|
15
13
|
endRev: changes[changes.length - 1].rev,
|
|
16
14
|
startRev,
|
|
17
15
|
...metadata
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import "../../chunk-IZ2YBCUP.js";
|
|
2
2
|
import { createVersionMetadata } from "../../data/version.js";
|
|
3
|
-
import { getISO, timestampDiff } from "../../utils/dates.js";
|
|
4
3
|
import { applyChanges } from "../shared/applyChanges.js";
|
|
5
4
|
import { breakChanges } from "../shared/changeBatching.js";
|
|
6
5
|
import { getStateAtRevision } from "./getStateAtRevision.js";
|
|
@@ -22,14 +21,14 @@ async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docI
|
|
|
22
21
|
let sessionStartIndex = 0;
|
|
23
22
|
for (let i = 1; i <= changes.length; i++) {
|
|
24
23
|
const isLastChange = i === changes.length;
|
|
25
|
-
const timeDiff = isLastChange ? Infinity :
|
|
24
|
+
const timeDiff = isLastChange ? Infinity : changes[i].createdAt - changes[i - 1].createdAt;
|
|
26
25
|
if (timeDiff > sessionTimeoutMillis || isLastChange) {
|
|
27
26
|
const sessionChanges = changes.slice(sessionStartIndex, i);
|
|
28
27
|
if (sessionChanges.length > 0) {
|
|
29
|
-
const isContinuation = !!lastVersion &&
|
|
28
|
+
const isContinuation = !!lastVersion && sessionChanges[0].createdAt - lastVersion.endedAt <= sessionTimeoutMillis;
|
|
30
29
|
if (isContinuation) {
|
|
31
30
|
const mergedState = applyChanges(offlineBaseState, sessionChanges);
|
|
32
|
-
const newEndedAt =
|
|
31
|
+
const newEndedAt = sessionChanges[sessionChanges.length - 1].createdAt;
|
|
33
32
|
const newRev = sessionChanges[sessionChanges.length - 1].rev;
|
|
34
33
|
await store.appendVersionChanges(docId, lastVersion.id, sessionChanges, newEndedAt, newRev, mergedState);
|
|
35
34
|
offlineBaseState = mergedState;
|
|
@@ -41,9 +40,8 @@ async function handleOfflineSessionsAndBatches(store, sessionTimeoutMillis, docI
|
|
|
41
40
|
groupId: batchId,
|
|
42
41
|
origin,
|
|
43
42
|
isOffline,
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
endedAt: getISO(sessionChanges[sessionChanges.length - 1].createdAt),
|
|
43
|
+
startedAt: sessionChanges[0].createdAt,
|
|
44
|
+
endedAt: sessionChanges[sessionChanges.length - 1].createdAt,
|
|
47
45
|
endRev: sessionChanges[sessionChanges.length - 1].rev,
|
|
48
46
|
startRev: sessionChanges[0].rev
|
|
49
47
|
});
|
package/dist/data/change.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import "../chunk-IZ2YBCUP.js";
|
|
2
2
|
import { inc } from "alphacounter";
|
|
3
3
|
import { createId } from "crypto-id";
|
|
4
|
-
import { getLocalISO } from "../utils/dates.js";
|
|
5
4
|
function createChangeId(rev) {
|
|
6
5
|
return inc.from(rev) + createId(4);
|
|
7
6
|
}
|
|
@@ -10,7 +9,7 @@ function createChange(baseRev, rev, ops, metadata) {
|
|
|
10
9
|
return {
|
|
11
10
|
id: createId(8),
|
|
12
11
|
ops: baseRev,
|
|
13
|
-
createdAt:
|
|
12
|
+
createdAt: Date.now(),
|
|
14
13
|
...rev
|
|
15
14
|
};
|
|
16
15
|
} else {
|
|
@@ -19,7 +18,7 @@ function createChange(baseRev, rev, ops, metadata) {
|
|
|
19
18
|
baseRev,
|
|
20
19
|
rev,
|
|
21
20
|
ops,
|
|
22
|
-
createdAt:
|
|
21
|
+
createdAt: Date.now(),
|
|
23
22
|
...metadata
|
|
24
23
|
};
|
|
25
24
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -18,7 +18,6 @@ export { transformPatch } from './json-patch/transformPatch.js';
|
|
|
18
18
|
export { JSONPatch, PathLike, WriteOptions } from './json-patch/JSONPatch.js';
|
|
19
19
|
export { ApplyJSONPatchOptions, JSONPatchOpHandlerMap as JSONPatchCustomTypes, JSONPatchOp } from './json-patch/types.js';
|
|
20
20
|
export { Branch, BranchStatus, Change, ChangeInput, ChangeMutator, CommitChangesOptions, DeleteDocOptions, DocumentTombstone, EditableBranchMetadata, EditableVersionMetadata, ListChangesOptions, ListVersionsOptions, PatchesSnapshot, PatchesState, PathProxy, SyncingState, VersionMetadata } from './types.js';
|
|
21
|
-
export { clampTimestamp, extractTimezoneOffset, getISO, getLocalISO, getLocalTimezoneOffset, timestampDiff } from './utils/dates.js';
|
|
22
21
|
export { add } from './json-patch/ops/add.js';
|
|
23
22
|
export { copy } from './json-patch/ops/copy.js';
|
|
24
23
|
export { increment } from './json-patch/ops/increment.js';
|
package/dist/index.js
CHANGED
|
@@ -31,7 +31,7 @@ declare class CompressedStoreBackend implements PatchesStoreBackend {
|
|
|
31
31
|
saveChanges(docId: string, changes: Change[]): Promise<void>;
|
|
32
32
|
listChanges(docId: string, options: ListChangesOptions): Promise<Change[]>;
|
|
33
33
|
createVersion(docId: string, metadata: VersionMetadata, state: any, changes: Change[]): Promise<void>;
|
|
34
|
-
appendVersionChanges(docId: string, versionId: string, changes: Change[], newEndedAt:
|
|
34
|
+
appendVersionChanges(docId: string, versionId: string, changes: Change[], newEndedAt: number, newRev: number, newState: any): Promise<void>;
|
|
35
35
|
loadVersionChanges(docId: string, versionId: string): Promise<Change[]>;
|
|
36
36
|
get loadLastVersionState(): PatchesStoreBackend['loadLastVersionState'];
|
|
37
37
|
get saveLastVersionState(): PatchesStoreBackend['saveLastVersionState'];
|
|
@@ -3,7 +3,6 @@ import { createId } from "crypto-id";
|
|
|
3
3
|
import { breakChanges } from "../algorithms/shared/changeBatching.js";
|
|
4
4
|
import { createChange } from "../data/change.js";
|
|
5
5
|
import { createVersionMetadata } from "../data/version.js";
|
|
6
|
-
import { getISO } from "../utils/dates.js";
|
|
7
6
|
class PatchesBranchManager {
|
|
8
7
|
constructor(store, patchesServer, maxPayloadBytes) {
|
|
9
8
|
this.store = store;
|
|
@@ -33,7 +32,7 @@ class PatchesBranchManager {
|
|
|
33
32
|
}
|
|
34
33
|
const stateAtRev = (await this.patchesServer.getStateAtRevision(docId, rev)).state;
|
|
35
34
|
const branchDocId = this.store.createBranchId ? await Promise.resolve(this.store.createBranchId(docId)) : createId(22);
|
|
36
|
-
const now =
|
|
35
|
+
const now = Date.now();
|
|
37
36
|
const initialVersionMetadata = createVersionMetadata({
|
|
38
37
|
origin: "main",
|
|
39
38
|
// Branch doc versions are 'main' until merged
|
|
@@ -7,7 +7,6 @@ import { applyChanges } from "../algorithms/shared/applyChanges.js";
|
|
|
7
7
|
import { createChange } from "../data/change.js";
|
|
8
8
|
import { signal } from "../event-signal.js";
|
|
9
9
|
import { createJSONPatch } from "../json-patch/createJSONPatch.js";
|
|
10
|
-
import { getISO } from "../utils/dates.js";
|
|
11
10
|
import { CompressedStoreBackend } from "./CompressedStoreBackend.js";
|
|
12
11
|
class PatchesServer {
|
|
13
12
|
sessionTimeoutMillis;
|
|
@@ -108,7 +107,7 @@ class PatchesServer {
|
|
|
108
107
|
const { rev: lastRev } = await this.getDoc(docId);
|
|
109
108
|
await this.store.createTombstone({
|
|
110
109
|
docId,
|
|
111
|
-
deletedAt:
|
|
110
|
+
deletedAt: Date.now(),
|
|
112
111
|
lastRev,
|
|
113
112
|
deletedByClientId: originClientId
|
|
114
113
|
});
|
package/dist/server/types.d.ts
CHANGED
|
@@ -27,7 +27,7 @@ interface PatchesStoreBackend {
|
|
|
27
27
|
* Appends changes to an existing version, updating its state snapshot, endedAt, and endRev.
|
|
28
28
|
* Used when a session spans multiple batch submissions.
|
|
29
29
|
*/
|
|
30
|
-
appendVersionChanges(docId: string, versionId: string, changes: Change[], newEndedAt:
|
|
30
|
+
appendVersionChanges(docId: string, versionId: string, changes: Change[], newEndedAt: number, newEndRev: number, newState: any): Promise<void>;
|
|
31
31
|
/** Lists version metadata based on filtering/sorting options. */
|
|
32
32
|
listVersions(docId: string, options: ListVersionsOptions): Promise<VersionMetadata[]>;
|
|
33
33
|
/** Loads the state snapshot for a specific version ID. */
|
package/dist/types.d.ts
CHANGED
|
@@ -17,8 +17,8 @@ interface ChangeInput {
|
|
|
17
17
|
baseRev?: number;
|
|
18
18
|
/** Optional revision number. If omitted, server assigns based on current state. */
|
|
19
19
|
rev?: number;
|
|
20
|
-
/**
|
|
21
|
-
createdAt:
|
|
20
|
+
/** Unix timestamp in milliseconds when the change was created. */
|
|
21
|
+
createdAt: number;
|
|
22
22
|
/** Optional batch identifier for grouping changes that belong to the same client batch (for multi-batch offline/large edits). */
|
|
23
23
|
batchId?: string;
|
|
24
24
|
/** Optional arbitrary metadata associated with the change. */
|
|
@@ -33,8 +33,8 @@ interface Change extends ChangeInput {
|
|
|
33
33
|
baseRev: number;
|
|
34
34
|
/** The revision number assigned by the server after commit. */
|
|
35
35
|
rev: number;
|
|
36
|
-
/**
|
|
37
|
-
committedAt:
|
|
36
|
+
/** Unix timestamp in milliseconds when the change was committed. */
|
|
37
|
+
committedAt: number;
|
|
38
38
|
}
|
|
39
39
|
/**
|
|
40
40
|
* Represents the state of a document in the OT protocol.
|
|
@@ -71,8 +71,8 @@ interface Branch {
|
|
|
71
71
|
docId: string;
|
|
72
72
|
/** The revision number on the source document where the branch occurred. */
|
|
73
73
|
branchedAtRev: number;
|
|
74
|
-
/**
|
|
75
|
-
createdAt:
|
|
74
|
+
/** Unix timestamp in milliseconds when the branch was created. */
|
|
75
|
+
createdAt: number;
|
|
76
76
|
/** Optional user-friendly name for the branch. */
|
|
77
77
|
name?: string;
|
|
78
78
|
/** Current status of the branch. */
|
|
@@ -89,14 +89,14 @@ type EditableBranchMetadata = Disallowed<Branch, 'id' | 'docId' | 'branchedAtRev
|
|
|
89
89
|
interface DocumentTombstone {
|
|
90
90
|
/** The ID of the deleted document. */
|
|
91
91
|
docId: string;
|
|
92
|
-
/**
|
|
93
|
-
deletedAt:
|
|
92
|
+
/** Unix timestamp in milliseconds when the document was deleted. */
|
|
93
|
+
deletedAt: number;
|
|
94
94
|
/** The last revision number before deletion. */
|
|
95
95
|
lastRev: number;
|
|
96
96
|
/** Optional client ID that initiated the deletion. */
|
|
97
97
|
deletedByClientId?: string;
|
|
98
|
-
/** Optional
|
|
99
|
-
expiresAt?:
|
|
98
|
+
/** Optional Unix timestamp in milliseconds for automatic tombstone expiration. */
|
|
99
|
+
expiresAt?: number;
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
102
|
* Options for deleting a document.
|
|
@@ -122,10 +122,10 @@ interface VersionMetadata {
|
|
|
122
122
|
isOffline?: boolean;
|
|
123
123
|
/** User-defined name if origin is 'branch'. */
|
|
124
124
|
branchName?: string;
|
|
125
|
-
/**
|
|
126
|
-
startedAt:
|
|
127
|
-
/**
|
|
128
|
-
endedAt:
|
|
125
|
+
/** Unix timestamp in milliseconds of version start. */
|
|
126
|
+
startedAt: number;
|
|
127
|
+
/** Unix timestamp in milliseconds of version end. */
|
|
128
|
+
endedAt: number;
|
|
129
129
|
/** The ending revision number of this version (the last change's rev). */
|
|
130
130
|
endRev: number;
|
|
131
131
|
/** The starting revision number of this version (the first change's rev). */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dabble/patches",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "Immutable JSON Patch implementation based on RFC 6902 supporting operational transformation and last-writer-wins",
|
|
5
5
|
"author": "Jacob Wright <jacwright@gmail.com>",
|
|
6
6
|
"bugs": {
|
package/dist/utils/dates.d.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Date utility functions for creating and manipulating ISO 8601 timestamps.
|
|
3
|
-
*
|
|
4
|
-
* Client-side timestamps use local timezone offsets (e.g., +04:00).
|
|
5
|
-
* Server-side timestamps use UTC with Z suffix.
|
|
6
|
-
*/
|
|
7
|
-
/**
|
|
8
|
-
* Converts a Date or ISO string to UTC format without milliseconds.
|
|
9
|
-
* Example: "2025-12-26T10:00:00Z"
|
|
10
|
-
*/
|
|
11
|
-
declare function getISO(date?: Date | string): string;
|
|
12
|
-
/**
|
|
13
|
-
* Formats a Date with a specific timezone offset string.
|
|
14
|
-
* The date is adjusted to display the correct local time for that offset.
|
|
15
|
-
*/
|
|
16
|
-
declare function getLocalISO(date?: Date | string, offset?: string): string;
|
|
17
|
-
/**
|
|
18
|
-
* Calculates milliseconds between two ISO timestamps.
|
|
19
|
-
* Returns (a - b) in milliseconds.
|
|
20
|
-
*/
|
|
21
|
-
declare function timestampDiff(a: string, b: string): number;
|
|
22
|
-
/**
|
|
23
|
-
* Clamps a timestamp to not exceed a limit, preserving the original timezone offset.
|
|
24
|
-
* Returns the original if it's <= limit, otherwise returns limit in original's timezone.
|
|
25
|
-
*
|
|
26
|
-
* Example:
|
|
27
|
-
* timestamp: "2025-12-26T18:00:00+04:00" (future)
|
|
28
|
-
* limit: "2025-12-26T10:00:00Z" (server time)
|
|
29
|
-
* result: "2025-12-26T14:00:00+04:00" (clamped, same instant as limit)
|
|
30
|
-
*/
|
|
31
|
-
declare function clampTimestamp(timestamp: string, limit: string): string;
|
|
32
|
-
/**
|
|
33
|
-
* Extracts the timezone offset string from an ISO timestamp.
|
|
34
|
-
* Returns "+04:00", "-05:00", or "Z".
|
|
35
|
-
*/
|
|
36
|
-
declare function extractTimezoneOffset(iso: string): string;
|
|
37
|
-
/**
|
|
38
|
-
* Gets the local timezone offset string for the current environment.
|
|
39
|
-
* Returns "+04:00", "-05:00", or "Z" for UTC.
|
|
40
|
-
*/
|
|
41
|
-
declare function getLocalTimezoneOffset(): string;
|
|
42
|
-
|
|
43
|
-
export { clampTimestamp, extractTimezoneOffset, getISO, getLocalISO, getLocalTimezoneOffset, timestampDiff };
|
package/dist/utils/dates.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import "../chunk-IZ2YBCUP.js";
|
|
2
|
-
function getISO(date = /* @__PURE__ */ new Date()) {
|
|
3
|
-
const d = typeof date === "string" ? new Date(date) : date;
|
|
4
|
-
return d.toISOString().replace(/\.\d{3}/, "");
|
|
5
|
-
}
|
|
6
|
-
function getLocalISO(date = /* @__PURE__ */ new Date(), offset = getLocalTimezoneOffset()) {
|
|
7
|
-
const match = offset.match(/([+-])(\d{2}):(\d{2})/);
|
|
8
|
-
if (offset === "Z" || !match) return getISO(date);
|
|
9
|
-
const sign = match[1] === "+" ? 1 : -1;
|
|
10
|
-
const offsetMinutes = sign * (parseInt(match[2]) * 60 + parseInt(match[3]));
|
|
11
|
-
const localDate = new Date((typeof date === "string" ? new Date(date) : date).getTime() + offsetMinutes * 60 * 1e3);
|
|
12
|
-
return getISO(localDate).slice(0, -1) + offset;
|
|
13
|
-
}
|
|
14
|
-
function timestampDiff(a, b) {
|
|
15
|
-
return new Date(a).getTime() - new Date(b).getTime();
|
|
16
|
-
}
|
|
17
|
-
function clampTimestamp(timestamp, limit) {
|
|
18
|
-
if (!timestamp || !limit) throw new Error("Timestamp and limit are required");
|
|
19
|
-
const timestampDate = new Date(timestamp);
|
|
20
|
-
const limitDate = new Date(limit);
|
|
21
|
-
if (timestampDate <= limitDate) {
|
|
22
|
-
return timestamp;
|
|
23
|
-
}
|
|
24
|
-
const offset = extractTimezoneOffset(timestamp);
|
|
25
|
-
return getLocalISO(limitDate, offset);
|
|
26
|
-
}
|
|
27
|
-
function extractTimezoneOffset(iso) {
|
|
28
|
-
if (!iso) return "Z";
|
|
29
|
-
const match = iso.match(/([+-]\d{2}:\d{2}|Z)$/);
|
|
30
|
-
return match ? match[1] : "Z";
|
|
31
|
-
}
|
|
32
|
-
function getLocalTimezoneOffset() {
|
|
33
|
-
const offset = -(/* @__PURE__ */ new Date()).getTimezoneOffset();
|
|
34
|
-
if (offset === 0) return "Z";
|
|
35
|
-
const hours = Math.floor(Math.abs(offset) / 60);
|
|
36
|
-
const mins = Math.abs(offset) % 60;
|
|
37
|
-
const sign = offset >= 0 ? "+" : "-";
|
|
38
|
-
return `${sign}${String(hours).padStart(2, "0")}:${String(mins).padStart(2, "0")}`;
|
|
39
|
-
}
|
|
40
|
-
export {
|
|
41
|
-
clampTimestamp,
|
|
42
|
-
extractTimezoneOffset,
|
|
43
|
-
getISO,
|
|
44
|
-
getLocalISO,
|
|
45
|
-
getLocalTimezoneOffset,
|
|
46
|
-
timestampDiff
|
|
47
|
-
};
|